Skip to content

Staking rewards: contract, claim pop-up, styled dashboard jurorinfo #1331

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

Closed
wants to merge 3 commits into from
Closed
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
6 changes: 6 additions & 0 deletions contracts/deploy/00-home-chain-arbitration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment)
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", pnk, 12225583, 12);
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", dai, 60327783, 11);
await execute("KlerosCore", { from: deployer, log: true }, "changeCurrencyRates", weth, 1, 1);

await deploy("MerkleRedeem", {
from: deployer,
args: [pnk],
log: true,
});
};

deployArbitration.tags = ["Arbitration"];
Expand Down
159 changes: 159 additions & 0 deletions contracts/src/arbitration/MerkleRedeem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: MIT

/**
* Original code taken from: https://github.com/balancer-labs/erc20-redeemable/blob/13d478a043ec7bfce7abefe708d027dfe3e2ea84/merkle/contracts/MerkleRedeem.sol
* Only comments and events were added, some variable names changed for clarity and the compiler version was upgraded to 0.8.x.
*/
pragma solidity 0.8.18;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title Distribution of tokens in a recurrent fashion.
contract MerkleRedeem is Ownable {
// ************************************* //
// * Enums / Structs * //
// ************************************* //

struct Claim {
uint week; // The week the claim is related to.
uint balance; // The amount being claimed.
bytes32[] merkleProof; // The merkle proof for the claim, sorted from the leaf to the root of the tree.
}

// ************************************* //
// * Events * //
// ************************************* //

/// @dev To be emitted when a claim is made.
/// @param _claimant The address of the claimant.
/// @param _balance The amount being claimed.
event Claimed(address _claimant, uint256 _balance);

// ************************************* //
// * Storage * //
// ************************************* //

IERC20 public token; // The address of the token being distributed.
mapping(uint => bytes32) public weekMerkleRoots; // The merkle roots of each week. weekMerkleRoots[week].
mapping(uint => mapping(address => bool)) public claimed; // Keeps track of the claim status for the given period and claimant. claimed[period][claimant].

// ************************************* //
// * Constructor * //
// ************************************* //

/// @param _token The address of the token being distributed.
constructor(address _token) {
token = IERC20(_token);
}

// ************************************* //
// * State Modifiers * //
// ************************************* //

/// @notice Seeds a new round for the airdrop.
/// @dev Will transfer tokens from the owner to this contract.
/// @param _week The airdrop week.
/// @param _merkleRoot The merkle root of the claims for that period.
/// @param _totalAllocation The amount of tokens allocated for the distribution.
function seedAllocations(uint _week, bytes32 _merkleRoot, uint _totalAllocation) external onlyOwner {
require(weekMerkleRoots[_week] == bytes32(0), "cannot rewrite merkle root");
weekMerkleRoots[_week] = _merkleRoot;

require(token.transferFrom(msg.sender, address(this), _totalAllocation), "ERR_TRANSFER_FAILED");
}

/// @notice Makes a claim for a given claimant in a week.
/// @param _liquidityProvider The address of the claimant.
/// @param _week The week for the claim.
/// @param _claimedBalance The amount being claimed.
/// @param _merkleProof The merkle proof for the claim, sorted from the leaf to the root of the tree.
function claimWeek(
address _liquidityProvider,
uint _week,
uint _claimedBalance,
bytes32[] memory _merkleProof
) public {
require(!claimed[_week][_liquidityProvider]);
require(verifyClaim(_liquidityProvider, _week, _claimedBalance, _merkleProof), "Incorrect merkle proof");

claimed[_week][_liquidityProvider] = true;
disburse(_liquidityProvider, _claimedBalance);
}

/// @notice Makes multiple claims for a given claimant.
/// @param _liquidityProvider The address of the claimant.
/// @param claims An array of claims containing the week, balance and the merkle proof.
function claimWeeks(address _liquidityProvider, Claim[] memory claims) public {
uint totalBalance = 0;
Claim memory claim;
for (uint i = 0; i < claims.length; i++) {
claim = claims[i];

require(!claimed[claim.week][_liquidityProvider]);
require(
verifyClaim(_liquidityProvider, claim.week, claim.balance, claim.merkleProof),
"Incorrect merkle proof"
);

totalBalance += claim.balance;
claimed[claim.week][_liquidityProvider] = true;
}
disburse(_liquidityProvider, totalBalance);
}

/// @notice Gets the claim status for given claimant from `_begin` to `_end` weeks.
/// @param _liquidityProvider The address of the claimant.
/// @param _begin The week to start with (inclusive).
/// @param _end The week to end with (inclusive).
function claimStatus(address _liquidityProvider, uint _begin, uint _end) external view returns (bool[] memory) {
uint size = 1 + _end - _begin;
bool[] memory arr = new bool[](size);
for (uint i = 0; i < size; i++) {
arr[i] = claimed[_begin + i][_liquidityProvider];
}
return arr;
}

/// @notice Gets all merkle roots for from `_begin` to `_end` weeks.
/// @param _begin The week to start with (inclusive).
/// @param _end The week to end with (inclusive).
function merkleRoots(uint _begin, uint _end) external view returns (bytes32[] memory) {
uint size = 1 + _end - _begin;
bytes32[] memory arr = new bytes32[](size);
for (uint i = 0; i < size; i++) {
arr[i] = weekMerkleRoots[_begin + i];
}
return arr;
}

/// @notice Verifies a claim.
/// @param _liquidityProvider The address of the claimant.
/// @param _week The week for the claim.
/// @param _claimedBalance The amount being claimed.
/// @param _merkleProof The merkle proof for the claim, sorted from the leaf to the root of the tree.
function verifyClaim(
address _liquidityProvider,
uint _week,
uint _claimedBalance,
bytes32[] memory _merkleProof
) public view returns (bool valid) {
bytes32 leaf = keccak256(abi.encodePacked(_liquidityProvider, _claimedBalance));
return MerkleProof.verify(_merkleProof, weekMerkleRoots[_week], leaf);
}

// ************************************* //
// * Internal * //
// ************************************* //

/// @dev Effectively pays a claimant.
/// @param _liquidityProvider The address of the claimant.
/// @param _balance The amount being claimed.
function disburse(address _liquidityProvider, uint _balance) private {
if (_balance > 0) {
emit Claimed(_liquidityProvider, _balance);
require(token.transfer(_liquidityProvider, _balance), "ERR_TRANSFER_FAILED");
}
}
}
3 changes: 3 additions & 0 deletions web/src/assets/svgs/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion web/src/assets/svgs/icons/kleros.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/ClaimedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import styled from "styled-components";

const StyledText = styled.text`
font-size: calc(20px + (24 - 20) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
font-weight: 600;
color: ${({ theme }) => theme.primaryText};
margin-top: 16px;
`;

const ClaimedText: React.FC = () => {
return <StyledText>🎉 Claimed! 🎉</StyledText>;
};
export default ClaimedText;
24 changes: 24 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/Close.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import styled from "styled-components";
import Icon from "tsx:svgs/icons/close.svg";

const StyledIcon = styled(Icon)`
position: absolute;
width: 18px;
height: 18px;
align-self: flex-end;
cursor: pointer;

path {
fill: ${({ theme }) => theme.stroke};
}
`;

interface IClose {
togglePopup: () => void;
}

const Close: React.FC<IClose> = ({ togglePopup }) => {
return <StyledIcon onClick={togglePopup}>CloseIcon</StyledIcon>;
};
export default Close;
16 changes: 16 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/Divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import styled from "styled-components";

const StyledHr = styled.hr`
display: flex;
border: none;
height: 0.5px;
background-color: ${({ theme }) => theme.stroke};
margin: calc(8px + (18 - 8) * (min(max(100vw, 375px), 1250px) - 375px) / 875) 0px;
width: 100%;
`;

const Divider: React.FC = () => {
return <StyledHr />;
};
export default Divider;
16 changes: 16 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/KlerosIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import styled from "styled-components";
import Icon from "svgs/icons/kleros.svg";

const StyledIcon = styled(Icon)`
path {
fill: ${({ theme }) => theme.secondaryPurple};
}
width: calc(120px + (160 - 120) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
height: calc(132px + (140 - 132) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
`;

const KlerosIcon: React.FC = () => {
return <StyledIcon />;
};
export default KlerosIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import styled from "styled-components";

const StyledText = styled.text`
font-size: calc(40px + (64 - 40) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
font-weight: 600;
color: ${({ theme }) => theme.secondaryPurple};
margin-top: 16px;
`;

const QuantityClaimed: React.FC = () => {
return <StyledText>1,000 PNK</StyledText>;
};
export default QuantityClaimed;
36 changes: 36 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/ReadMore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import styled from "styled-components";
import RightArrow from "tsx:svgs/icons/arrow.svg";

const StyledLink = styled.a`
display: flex;
color: ${({ theme }) => theme.primaryBlue};
font-size: 16px;
margin-top: 8px;
align-items: center;
gap: 8px;

&:hover {
text-decoration: underline;
}
`;

const StyledRightArrow = styled(RightArrow)`
path {
fill: ${({ theme }) => theme.primaryBlue};
}
`;

const ReadMore: React.FC = () => {
return (
<StyledLink
href="https://blog.kleros.io/the-launch-of-the-kleros-juror-incentive-program/"
target="_blank"
rel="noreferrer"
>
<text>Read more about the Juror Incentive Program</text>
<StyledRightArrow />
</StyledLink>
);
};
export default ReadMore;
13 changes: 13 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/ThanksText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import styled from "styled-components";

const StyledText = styled.text`
font-size: 16px;
color: ${({ theme }) => theme.primaryText};
margin-top: 16px;
`;

const ThanksText: React.FC = () => {
return <StyledText>Thank you for being part of the Kleros community.</StyledText>;
};
export default ThanksText;
63 changes: 63 additions & 0 deletions web/src/components/Popup/ClaimedStakingRewards/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useRef } from "react";
import styled, { css } from "styled-components";
import { landscapeStyle } from "styles/landscapeStyle";
import { useFocusOutside } from "hooks/useFocusOutside";
import { Overlay } from "components/Overlay";
import KlerosIcon from "./KlerosIcon";
import ClaimedText from "./ClaimedText";
import QuantityClaimed from "./QuantityClaimed";
import Divider from "./Divider";
import ThanksText from "./ThanksText";
import ReadMore from "./ReadMore";
import Close from "./Close";

const Container = styled.div`
display: flex;
position: relative;
width: 86vw;
flex-direction: column;
align-items: center;
background-color: ${({ theme }) => theme.whiteBackground};
padding: calc(24px + (52 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
padding-top: calc(24px + (48 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);

position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-height: 80vh;
overflow-y: auto;
z-index: 10;

${landscapeStyle(
() => css`
width: calc(300px + (862 - 300) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
`
)}
`;

interface IClaimedStakingRewards {
toggleIsOpen: () => void;
}

const ClaimedStakingRewards: React.FC<IClaimedStakingRewards> = ({ toggleIsOpen }) => {
const containerRef = useRef(null);
useFocusOutside(containerRef, () => toggleIsOpen());

return (
<>
<Overlay />
<Container ref={containerRef}>
<KlerosIcon />
<ClaimedText />
<QuantityClaimed />
<Divider />
<ThanksText />
<ReadMore />
<Close togglePopup={toggleIsOpen} />
</Container>
</>
);
};

export default ClaimedStakingRewards;
3 changes: 3 additions & 0 deletions web/src/components/Popup/Description/StakeWithdraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const Container = styled.div`
`;

const StyledKlerosLogo = styled(KlerosLogo)`
path {
fill: ${({ theme }) => theme.secondaryPurple};
}
width: 14px;
height: 14px;
`;
Expand Down
Loading