Skip to content

Commit

Permalink
move to centralized factory with claim for many pools
Browse files Browse the repository at this point in the history
  • Loading branch information
coffeexcoin committed Oct 11, 2024
1 parent a1e0eed commit 1ea0132
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 107 deletions.
110 changes: 3 additions & 107 deletions src/staking/DyadLPStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,27 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {OwnableRoles} from "solady/auth/OwnableRoles.sol";
import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {IVaultManager} from "../interfaces/IVaultManager.sol";
import {IExtension} from "../interfaces/IExtension.sol";

contract DyadLPStaking is OwnableRoles, IExtension {
contract DyadLPStaking is OwnableRoles {
using SafeTransferLib for address;
using FixedPointMathLib for uint256;

error NotOwnerOfNote();
error InvalidProof();
error InvalidBlockNumber();
error NotAllowed();

event Claimed(uint256 indexed noteId, uint256 indexed amount, uint256 unclaimedBonus);
event Deposited(uint256 indexed noteId, uint256 indexed amount);
event Withdrawn(uint256 indexed noteId, uint256 indexed amount);
event RootUpdated(bytes32 newRoot, uint256 blockNumber);
event RewardsDeposited(uint256 amount);

uint256 public constant MANAGER_ROLE = _ROLE_0;

address public immutable lpToken;
address public immutable kerosene;
IERC721 public immutable dnft;
address public immutable keroseneVault;
IVaultManager public immutable vaultManager;

bytes32 public merkleRoot;
uint256 public totalLP;
uint256 public unclaimedBonus;
uint256 public lastUpdateBlock;

mapping(uint256 noteId => uint256 amount) public noteIdToAmountDeposited;
mapping(uint256 noteId => uint256 amount) public noteIdToTotalClaimed;

constructor(address _lpToken, address _kerosene, address _dnft, address _keroseneVault, address _vaultManager) {
constructor(address _lpToken, address _dnft, address _owner) {
lpToken = _lpToken;
kerosene = _kerosene;
dnft = IERC721(_dnft);
keroseneVault = _keroseneVault;
vaultManager = IVaultManager(_vaultManager);
_initializeOwner(msg.sender);
}

function name() public view override returns (string memory) {
return string.concat("Dyad ", IERC20(lpToken).symbol(), " LP Staking");
}

function description() public view override returns (string memory) {
return string.concat("Stake ", IERC20(lpToken).symbol(), " tokens to earn Kerosene");
}

function getHookFlags() public pure override returns (uint256) {
return 0;
_initializeOwner(_owner);
}

function deposit(uint256 noteId, uint256 amount) public {
Expand All @@ -80,84 +48,12 @@ contract DyadLPStaking is OwnableRoles, IExtension {
emit Withdrawn(noteId, amount);
}

function setRoot(bytes32 _merkleRoot, uint256 blockNumber) public onlyRoles(MANAGER_ROLE) {
if (blockNumber > lastUpdateBlock) {
revert InvalidBlockNumber();
}
merkleRoot = _merkleRoot;
lastUpdateBlock = blockNumber;

emit RootUpdated(_merkleRoot, blockNumber);
}

function claim(uint256 noteId, uint256 amount, bytes32[] calldata proof) public returns (uint256) {
address noteOwner = dnft.ownerOf(noteId);
require(msg.sender == noteOwner, NotOwnerOfNote());

_verifyProof(noteId, amount, proof);
uint256 amountToSend = _syncClaimableAmount(noteId, amount);
uint256 claimSubBonus = amountToSend.mulDiv(80, 100);
uint256 unclaimed = amountToSend - claimSubBonus;
unclaimedBonus += unclaimed;

kerosene.safeTransfer(noteOwner, claimSubBonus);

emit Claimed(noteId, claimSubBonus, unclaimed);

return claimSubBonus;
}

function claimToVault(uint256 noteId, uint256 amount, bytes32[] calldata proof) public returns (uint256) {
require(msg.sender == dnft.ownerOf(noteId), NotOwnerOfNote());

_verifyProof(noteId, amount, proof);
uint256 amountToSend = _syncClaimableAmount(noteId, amount);

kerosene.safeApprove(address(vaultManager), amountToSend);
vaultManager.deposit(noteId, keroseneVault, amountToSend);

emit Claimed(noteId, amountToSend, 0);

return amountToSend;
}

function _verifyProof(uint256 noteId, uint256 amount, bytes32[] calldata proof) internal view {
// double hash to prevent second preimage attack
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(noteId, amount))));
require(MerkleProofLib.verifyCalldata(proof, merkleRoot, leaf), InvalidProof());
}

function _syncClaimableAmount(uint256 noteId, uint256 amount) private returns (uint256) {
uint256 alreadyClaimed = noteIdToTotalClaimed[noteId];
uint256 amountToSend = amount - alreadyClaimed;
if (amountToSend == 0) {
return 0;
}
noteIdToTotalClaimed[noteId] += amountToSend;

return amountToSend;
}

function depositForRewards(uint256 amount) public onlyOwnerOrRoles(MANAGER_ROLE) {
uint256 previousUnclaimedBonus = unclaimedBonus;
unclaimedBonus = 0;
if (amount < previousUnclaimedBonus) {
kerosene.safeTransfer(msg.sender, previousUnclaimedBonus - amount);
} else if (amount > previousUnclaimedBonus) {
kerosene.safeTransferFrom(msg.sender, address(this), amount - previousUnclaimedBonus);
}

emit RewardsDeposited(amount);
}

function recoverERC20(address token) public onlyOwner {
uint256 amount = IERC20(token).balanceOf(address(this));
if (token == address(lpToken)) {
// lpToken is staked by users so the only amount that should be recoverable is tokens
// that are sent accidentally without using the deposit function
amount -= totalLP;
} else if (token == address(kerosene)) {
revert NotAllowed();
}
token.safeTransfer(msg.sender, amount);
}
Expand Down
194 changes: 194 additions & 0 deletions src/staking/LPStakingFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {Ownable} from "solady/auth/Ownable.sol";
import {DyadLPStaking} from "./DyadLPStaking.sol";
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {IERC721} from "forge-std/interfaces/IERC721.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {OwnableRoles} from "solady/auth/OwnableRoles.sol";
import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {IExtension} from "../interfaces/IExtension.sol";
import {IVaultManager} from "../interfaces/IVaultManager.sol";

contract LPStakingFactory is OwnableRoles, IExtension {
using SafeTransferLib for address;
using FixedPointMathLib for uint256;
using EnumerableSetLib for EnumerableSetLib.AddressSet;

error NotAllowed();
error InvalidProof();
error NotOwnerOfNote();
error InvalidBlockNumber();
error Paused();

event PoolStakingCreated(address indexed lpToken, address indexed staking);
event RewardRateSet(address indexed lpToken, uint256 oldRewardRate, uint256 newRewardRate);
event Claimed(uint256 indexed noteId, uint256 indexed amount, uint256 unclaimedBonus);
event RootUpdated(bytes32 newRoot, uint256 blockNumber);
event RewardsDeposited(uint256 amount);

uint256 public constant REWARDS_MANAGER_ROLE = _ROLE_0;
uint256 public constant POOL_MANAGER_ROLE = _ROLE_1;

address public immutable kerosene;
IERC721 public immutable dnft;
address public immutable keroseneVault;
IVaultManager public immutable vaultManager;

/// @notice lpToken to staking contract
mapping(address lpToken => address staking) public lpTokenToStaking;

/// @notice reward rate in kerosene per second emitted to the pool
mapping(address lpToken => uint256 rewardRate) public lpTokenToRewardRate;

/// @notice total amount of rewards claimed for a note
mapping(uint256 noteId => uint256 amount) public noteIdToTotalClaimed;

/// @notice merkle root for rewards distribution
bytes32 public merkleRoot;

/// @notice last block number when the root was updated
uint256 public lastUpdateBlock;

/// @notice forfited bonus in kerosene
uint248 public unclaimedBonus;

/// @notice indicates whether claiming is paused
bool public paused;

modifier whenNotPaused() {
require(!paused, Paused());
_;
}

constructor(address _kerosene, address _dnft, address _keroseneVault, address _vaultManager) {
kerosene = _kerosene;
dnft = IERC721(_dnft);
keroseneVault = _keroseneVault;
vaultManager = IVaultManager(_vaultManager);
_initializeOwner(msg.sender);
}

function name() public pure override returns (string memory) {
return "Dyad LP Staking Rewards";
}

function description() public pure override returns (string memory) {
return "Claim Kerosene rewards for staked LP tokens";
}

function getHookFlags() public pure override returns (uint256) {
return 0;
}

function createPoolStaking(address _lpToken) external onlyOwnerOrRoles(POOL_MANAGER_ROLE) returns (address) {
DyadLPStaking staking = new DyadLPStaking(_lpToken, address(dnft), owner());

lpTokenToStaking[_lpToken] = address(staking);
emit PoolStakingCreated(_lpToken, address(staking));
return address(staking);
}

function setPaused(bool _paused) public onlyOwnerOrRoles(POOL_MANAGER_ROLE) {
paused = _paused;
}

function setRewardRates(address[] calldata lpTokens, uint256[] calldata rewardRates)
external
onlyOwnerOrRoles(REWARDS_MANAGER_ROLE)
{
uint256 length = lpTokens.length;
for (uint256 i; i < length; ++i) {
address lpToken = lpTokens[i];
uint256 rewardRate = rewardRates[i];
uint256 oldRewardRate = lpTokenToRewardRate[lpToken];
emit RewardRateSet(lpToken, oldRewardRate, rewardRate);
lpTokenToRewardRate[lpToken] = rewardRate;
}
}

function depositForRewards(uint256 amount) public onlyOwnerOrRoles(REWARDS_MANAGER_ROLE) {
uint256 previousUnclaimedBonus = unclaimedBonus;
unclaimedBonus = 0;
if (amount < previousUnclaimedBonus) {
kerosene.safeTransfer(msg.sender, previousUnclaimedBonus - amount);
} else if (amount > previousUnclaimedBonus) {
kerosene.safeTransferFrom(msg.sender, address(this), amount - previousUnclaimedBonus);
}

emit RewardsDeposited(amount);
}

function setRoot(bytes32 _merkleRoot, uint256 blockNumber) public onlyOwnerOrRoles(REWARDS_MANAGER_ROLE) {
if (blockNumber > lastUpdateBlock) {
revert InvalidBlockNumber();
}
merkleRoot = _merkleRoot;
lastUpdateBlock = blockNumber;

emit RootUpdated(_merkleRoot, blockNumber);
}

function claim(uint256 noteId, uint256 amount, bytes32[] calldata proof) public whenNotPaused returns (uint256) {
address noteOwner = dnft.ownerOf(noteId);
require(msg.sender == noteOwner, NotOwnerOfNote());

_verifyProof(noteId, amount, proof);
uint256 amountToSend = _syncClaimableAmount(noteId, amount);
uint256 claimSubBonus = amountToSend.mulDiv(80, 100);
uint256 unclaimed = amountToSend - claimSubBonus;
unclaimedBonus += unclaimed;

kerosene.safeTransfer(noteOwner, claimSubBonus);

emit Claimed(noteId, claimSubBonus, unclaimed);

return claimSubBonus;
}

function claimToVault(uint256 noteId, uint256 amount, bytes32[] calldata proof)
public
whenNotPausedreturns(uint256)
{
require(msg.sender == dnft.ownerOf(noteId), NotOwnerOfNote());

_verifyProof(noteId, amount, proof);
uint256 amountToSend = _syncClaimableAmount(noteId, amount);

kerosene.safeApprove(address(vaultManager), amountToSend);
vaultManager.deposit(noteId, keroseneVault, amountToSend);

emit Claimed(noteId, amountToSend, 0);

return amountToSend;
}

function _verifyProof(uint256 noteId, uint256 amount, bytes32[] calldata proof) internal view {
// double hash to prevent second preimage attack
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(noteId, amount))));
require(MerkleProofLib.verifyCalldata(proof, merkleRoot, leaf), InvalidProof());
}

function _syncClaimableAmount(uint256 noteId, uint256 amount) private returns (uint256) {
uint256 alreadyClaimed = noteIdToTotalClaimed[noteId];
uint256 amountToSend = amount - alreadyClaimed;
if (amountToSend == 0) {
return 0;
}
noteIdToTotalClaimed[noteId] += amountToSend;

return amountToSend;
}

function recoverERC20(address token) public onlyOwner {
uint256 amount = IERC20(token).balanceOf(address(this));
if (token == address(kerosene)) {
revert NotAllowed();
}
token.safeTransfer(msg.sender, amount);
}

function recoverERC721(address token, uint256 tokenId) public onlyOwner {
IERC721(token).transferFrom(address(this), msg.sender, tokenId);
}
}

0 comments on commit 1ea0132

Please sign in to comment.