diff --git a/contracts/deploy/00-home-chain-arbitration.ts b/contracts/deploy/00-home-chain-arbitration.ts
index bf5b961bb..e1a1372d6 100644
--- a/contracts/deploy/00-home-chain-arbitration.ts
+++ b/contracts/deploy/00-home-chain-arbitration.ts
@@ -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"];
diff --git a/contracts/src/arbitration/MerkleRedeem.sol b/contracts/src/arbitration/MerkleRedeem.sol
new file mode 100644
index 000000000..ad8ffaa38
--- /dev/null
+++ b/contracts/src/arbitration/MerkleRedeem.sol
@@ -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");
+ }
+ }
+}
diff --git a/web/src/assets/svgs/icons/close.svg b/web/src/assets/svgs/icons/close.svg
new file mode 100644
index 000000000..ab7bfa063
--- /dev/null
+++ b/web/src/assets/svgs/icons/close.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/assets/svgs/icons/kleros.svg b/web/src/assets/svgs/icons/kleros.svg
index e0dc1a0fd..a674a9235 100644
--- a/web/src/assets/svgs/icons/kleros.svg
+++ b/web/src/assets/svgs/icons/kleros.svg
@@ -1,3 +1,3 @@
diff --git a/web/src/components/Popup/ClaimedStakingRewards/ClaimedText.tsx b/web/src/components/Popup/ClaimedStakingRewards/ClaimedText.tsx
new file mode 100644
index 000000000..729f82e90
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/ClaimedText.tsx
@@ -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 🎉 Claimed! 🎉;
+};
+export default ClaimedText;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/Close.tsx b/web/src/components/Popup/ClaimedStakingRewards/Close.tsx
new file mode 100644
index 000000000..c784d8637
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/Close.tsx
@@ -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 = ({ togglePopup }) => {
+ return CloseIcon;
+};
+export default Close;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/Divider.tsx b/web/src/components/Popup/ClaimedStakingRewards/Divider.tsx
new file mode 100644
index 000000000..388938795
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/Divider.tsx
@@ -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 ;
+};
+export default Divider;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/KlerosIcon.tsx b/web/src/components/Popup/ClaimedStakingRewards/KlerosIcon.tsx
new file mode 100644
index 000000000..58e856269
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/KlerosIcon.tsx
@@ -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 ;
+};
+export default KlerosIcon;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/QuantityClaimed.tsx b/web/src/components/Popup/ClaimedStakingRewards/QuantityClaimed.tsx
new file mode 100644
index 000000000..177f36487
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/QuantityClaimed.tsx
@@ -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 1,000 PNK;
+};
+export default QuantityClaimed;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/ReadMore.tsx b/web/src/components/Popup/ClaimedStakingRewards/ReadMore.tsx
new file mode 100644
index 000000000..a060c89d4
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/ReadMore.tsx
@@ -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 (
+
+ Read more about the Juror Incentive Program
+
+
+ );
+};
+export default ReadMore;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/ThanksText.tsx b/web/src/components/Popup/ClaimedStakingRewards/ThanksText.tsx
new file mode 100644
index 000000000..d47ddce42
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/ThanksText.tsx
@@ -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 Thank you for being part of the Kleros community.;
+};
+export default ThanksText;
diff --git a/web/src/components/Popup/ClaimedStakingRewards/index.tsx b/web/src/components/Popup/ClaimedStakingRewards/index.tsx
new file mode 100644
index 000000000..7baea3051
--- /dev/null
+++ b/web/src/components/Popup/ClaimedStakingRewards/index.tsx
@@ -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 = ({ toggleIsOpen }) => {
+ const containerRef = useRef(null);
+ useFocusOutside(containerRef, () => toggleIsOpen());
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ClaimedStakingRewards;
diff --git a/web/src/components/Popup/Description/StakeWithdraw.tsx b/web/src/components/Popup/Description/StakeWithdraw.tsx
index e781a462b..078353bc6 100644
--- a/web/src/components/Popup/Description/StakeWithdraw.tsx
+++ b/web/src/components/Popup/Description/StakeWithdraw.tsx
@@ -13,6 +13,9 @@ const Container = styled.div`
`;
const StyledKlerosLogo = styled(KlerosLogo)`
+ path {
+ fill: ${({ theme }) => theme.secondaryPurple};
+ }
width: 14px;
height: 14px;
`;
diff --git a/web/src/layout/Header/navbar/DappList.tsx b/web/src/layout/Header/navbar/DappList.tsx
index 9aca796d5..4a361b646 100644
--- a/web/src/layout/Header/navbar/DappList.tsx
+++ b/web/src/layout/Header/navbar/DappList.tsx
@@ -58,6 +58,12 @@ const Container = styled.div`
)}
`;
+const StyledCourt = styled(Court)`
+ path {
+ fill: ${({ theme }) => theme.secondaryPurple};
+ }
+`;
+
const ItemsDiv = styled.div`
display: grid;
overflow-y: auto;
@@ -74,7 +80,7 @@ const ItemsDiv = styled.div`
const ITEMS = [
{
text: "Court v1",
- Icon: Court,
+ Icon: StyledCourt,
url: "https://court.kleros.io/",
},
{
diff --git a/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx b/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx
index 2c8653e5a..3df08ad89 100644
--- a/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx
+++ b/web/src/pages/Dashboard/JurorInfo/JurorRewards.tsx
@@ -13,6 +13,7 @@ const Container = styled.div`
flex-direction: column;
align-items: flex-start;
width: auto;
+ gap: calc(8px + (32 - 8) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
`;
const tooltipMsg =
diff --git a/web/src/pages/Dashboard/JurorInfo/StakingRewards.tsx b/web/src/pages/Dashboard/JurorInfo/StakingRewards.tsx
index 119d1d3e0..ffaee12a2 100644
--- a/web/src/pages/Dashboard/JurorInfo/StakingRewards.tsx
+++ b/web/src/pages/Dashboard/JurorInfo/StakingRewards.tsx
@@ -1,14 +1,17 @@
import React from "react";
import styled from "styled-components";
+import { useToggle } from "react-use";
import { Box as _Box, Button } from "@kleros/ui-components-library";
import TokenRewards from "./TokenRewards";
import WithHelpTooltip from "../WithHelpTooltip";
+import ClaimedStakingRewards from "components/Popup/ClaimedStakingRewards";
import { EnsureChain } from "components/EnsureChain";
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
+ gap: calc(8px + (32 - 8) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
`;
const Box = styled(_Box)`
@@ -16,9 +19,13 @@ const Box = styled(_Box)`
justify-content: space-between;
align-items: center;
padding: 8px;
- width: 270px;
+ padding-left: 20px;
+ width: calc(232px + (312 - 232) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
height: auto;
+ border: 1px solid ${({ theme }) => theme.stroke};
border-radius: 3px;
+ background-color: ${({ theme }) => theme.lightBlue};
+ gap: calc(12px + (28 - 12) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
`;
const UnclaimedContainer = styled.div`
@@ -27,25 +34,36 @@ const UnclaimedContainer = styled.div`
gap: 4px;
`;
+const StyledSmall = styled.small`
+ font-size: 16px;
+`;
+
const ClaimPNK: React.FC = () => {
+ const [isClaimRewardsModalOpen, toggleClaimRewardsModal] = useToggle(false);
+
return (
-
-
-
- 1,000 PNK
-
-
-
-
-
+ <>
+
+
+
+ 1,000 PNK
+
+
+
+
+
+ {isClaimRewardsModalOpen && }
+ >
);
};
const tooltipMsg =
"Staking Rewards are the rewards won by staking your PNK on a court during " +
- "the Kleros' Jurors incentive program.";
+ "the Kleros' Jurors incentive program." +
+ " APY means Annual Percentage Yield, and it is the rate of interest earned" +
+ " on your staked PNK in one year.";
-const Coherency: React.FC = () => {
+const StakingRewards: React.FC = () => {
return (
@@ -59,4 +77,4 @@ const Coherency: React.FC = () => {
);
};
-export default Coherency;
+export default StakingRewards;
diff --git a/web/src/pages/Dashboard/JurorInfo/index.tsx b/web/src/pages/Dashboard/JurorInfo/index.tsx
index 84eda39b9..ecce3a1de 100644
--- a/web/src/pages/Dashboard/JurorInfo/index.tsx
+++ b/web/src/pages/Dashboard/JurorInfo/index.tsx
@@ -9,7 +9,7 @@ import PixelArt from "./PixelArt";
import { useAccount } from "wagmi";
import { useUserQuery } from "queries/useUser";
import { getUserLevelData } from "utils/userLevelCalculation";
-// import StakingRewards from "./StakingRewards";
+import StakingRewards from "./StakingRewards";
const Container = styled.div``;
@@ -17,18 +17,33 @@ const Card = styled(_Card)`
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
+ justify-content: flex-start;
+ flex-wrap: wrap;
- gap: 40px;
+ gap: 52px;
width: 100%;
height: auto;
- padding: 24px 0;
+ padding: 24px 32px;
${landscapeStyle(
() => css`
flex-direction: row;
- gap: calc(24px + (64 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
- height: 236px;
+ gap: 68px calc(24px + (96 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
+ min-height: 236px;
+ `
+ )}
+`;
+
+const PixelArtAndCoherency = styled.div`
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 16px;
+
+ ${landscapeStyle(
+ () => css`
+ flex-direction: row;
+ gap: 32px;
`
)}
`;
@@ -51,14 +66,17 @@ const JurorInfo: React.FC = () => {
totalResolvedDisputes={totalResolvedDisputes}
/>
-
-
+
+
+
+
+
);