Skip to content

Instantly share code, notes, and snippets.

@andrecronje
Last active June 18, 2022 03:05
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save andrecronje/211f1a18857efd2fc8359e929f076ceb to your computer and use it in GitHub Desktop.
Save andrecronje/211f1a18857efd2fc8359e929f076ceb to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;
library Math {
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a >= b ? a : b;
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
library PoolAddress {
bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;
struct PoolKey {
address token0;
address token1;
uint24 fee;
}
function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
require(key.token0 < key.token1);
pool = address(
uint160(uint256(
keccak256(
abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
));
}
}
interface erc20 {
function transfer(address recipient, uint amount) external returns (bool);
function transferFrom(address sender, address recipient, uint amount) external returns (bool);
}
interface PositionManagerV3 {
function positions(uint256 tokenId)
external
view
returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
function safeTransferFrom(address from, address to, uint tokenId) external;
function ownerOf(uint tokenId) external view returns (address);
function transferFrom(address from, address to, uint tokenId) external;
}
interface UniV3 {
function snapshotCumulativesInside(int24 tickLower, int24 tickUpper)
external
view
returns (
int56 tickCumulativeInside,
uint160 secondsPerLiquidityInsideX128,
uint32 secondsInside
);
function liquidity() external view returns (uint128);
}
contract StakingRewardsV3 {
address immutable public reward;
address immutable public pool;
address constant factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
PositionManagerV3 constant nftManager = PositionManagerV3(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
uint constant DURATION = 7 days;
uint rewardRate;
uint periodFinish;
uint lastUpdateTime;
uint rewardPerSecondStored;
mapping(uint => uint) public tokenRewardPerSecondPaid;
mapping(uint => uint) public rewards;
struct time {
uint32 timestamp;
uint160 secondsPerLiquidityInside;
}
mapping(uint => time) public elapsed;
mapping(uint => address) public owners;
mapping(address => uint[]) public tokenIds;
mapping(uint => uint) public liquidityOf;
event RewardPaid(address indexed sender, uint tokenId, uint reward);
event RewardAdded(address indexed sender, uint reward);
event Deposit(address indexed sender, uint tokenId, uint liquidity);
event Withdraw(address indexed sender, uint tokenId, uint liquidity);
constructor(address _reward, address _pool) {
reward = _reward;
pool = _pool;
}
function getTokenIds(address owner) external view returns (uint[] memory) {
return tokenIds[owner];
}
function lastTimeRewardApplicable() public view returns (uint) {
return Math.min(block.timestamp, periodFinish);
}
function rewardPerSecond() public view returns (uint) {
return rewardPerSecondStored + ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate);
}
function earned(uint tokenId) public view returns (uint claimable, uint160 secondsPerLiquidityInside) {
uint _reward = rewardPerSecond() - tokenRewardPerSecondPaid[tokenId];
time memory _elapsed = elapsed[tokenId];
uint secondsInside;
(secondsPerLiquidityInside, secondsInside) = _getSecondsInside(tokenId);
uint _maxSecondsInside = lastUpdateTime - Math.min(_elapsed.timestamp, periodFinish);
uint _secondsInside = Math.min((secondsPerLiquidityInside - _elapsed.secondsPerLiquidityInside) * liquidityOf[tokenId], _maxSecondsInside);
if (secondsInside > _maxSecondsInside && _secondsInside > 0) {
_secondsInside = _secondsInside * _maxSecondsInside / secondsInside;
}
claimable = (_reward * _secondsInside) + rewards[tokenId];
}
function getRewardForDuration() external view returns (uint) {
return rewardRate * DURATION;
}
function deposit(uint tokenId) external update(tokenId) {
(,,address token0, address token1,uint24 fee,int24 tickLower,int24 tickUpper,uint128 _liquidity,,,,) = nftManager.positions(tokenId);
address _pool = PoolAddress.computeAddress(factory,PoolAddress.PoolKey({token0: token0, token1: token1, fee: fee}));
(,uint160 _secondsPerLiquidityInside,) = UniV3(pool).snapshotCumulativesInside(tickLower, tickUpper);
require(pool == _pool);
require(_liquidity > 0);
liquidityOf[tokenId] = _liquidity;
elapsed[tokenId] = time(uint32(lastTimeRewardApplicable()), _secondsPerLiquidityInside);
owners[tokenId] = msg.sender;
tokenIds[msg.sender].push(tokenId);
nftManager.transferFrom(msg.sender, address(this), tokenId);
emit Deposit(msg.sender, tokenId, _liquidity);
}
function _findIndex(uint[] memory array, uint element) internal pure returns (uint i) {
for (i = 0; i < array.length; i++) {
if (array[i] == element) {
break;
}
}
}
function _remove(uint[] storage array, uint element) internal {
uint index = _findIndex(array, element);
if (index >= array.length) return;
for (uint i = index; i < array.length-1; i++){
array[i] = array[i+1];
}
array.pop();
}
function withdraw(uint tokenId) public update(tokenId) {
require(owners[tokenId] == msg.sender);
uint _liquidity = liquidityOf[tokenId];
liquidityOf[tokenId] = 0;
owners[tokenId] = address(0);
_remove(tokenIds[msg.sender], tokenId);
nftManager.safeTransferFrom(address(this), msg.sender, tokenId);
emit Deposit(msg.sender, tokenId, _liquidity);
}
function getRewards() external {
uint[] memory _tokens = tokenIds[msg.sender];
for (uint i = 0; i < _tokens.length; i++) {
getReward(_tokens[i]);
}
}
function getReward(uint tokenId) public update(tokenId) {
uint _reward = rewards[tokenId];
if (_reward > 0) {
rewards[tokenId] = 0;
_safeTransfer(reward, _getRecipient(tokenId), _reward);
emit RewardPaid(msg.sender, tokenId, _reward);
}
}
function _getRecipient(uint tokenId) internal view returns (address) {
if (owners[tokenId] != address(0)) {
return owners[tokenId];
} else {
return nftManager.ownerOf(tokenId);
}
}
function exit() external {
uint[] memory _tokens = tokenIds[msg.sender];
for (uint i = 0; i < _tokens.length; i++) {
if (nftManager.ownerOf(_tokens[i]) == address(this)) {
withdraw(_tokens[i]);
}
getReward(_tokens[i]);
}
}
function exit(uint tokenId) public {
withdraw(tokenId);
getReward(tokenId);
}
function notify(uint amount) external update(0) {
_safeTransferFrom(reward, msg.sender, address(this), amount);
if (block.timestamp >= periodFinish) {
rewardRate = amount / DURATION;
} else {
uint _remaining = periodFinish - block.timestamp;
uint _leftover = _remaining * rewardRate;
rewardRate = (amount + _leftover) / DURATION;
}
lastUpdateTime = block.timestamp;
periodFinish = block.timestamp + DURATION;
emit RewardAdded(msg.sender, amount);
}
modifier update(uint tokenId) {
rewardPerSecondStored = rewardPerSecond();
lastUpdateTime = lastTimeRewardApplicable();
if (tokenId != 0) {
(uint _reward, uint160 _secondsPerLiquidityInside) = earned(tokenId);
tokenRewardPerSecondPaid[tokenId] = rewardPerSecondStored;
rewards[tokenId] = _reward;
if (elapsed[tokenId].timestamp < lastUpdateTime) {
elapsed[tokenId] = time(uint32(lastUpdateTime), _secondsPerLiquidityInside);
}
}
_;
}
function _getSecondsInside(uint256 tokenId) internal view returns (uint160 secondsPerLiquidityInside, uint secondsInside) {
(,,,,,int24 tickLower,int24 tickUpper,,,,,) = nftManager.positions(tokenId);
(,secondsPerLiquidityInside,secondsInside) = UniV3(pool).snapshotCumulativesInside(tickLower, tickUpper);
}
function _safeTransfer(address token, address to, uint256 value) internal {
(bool success, bytes memory data) =
token.call(abi.encodeWithSelector(erc20.transfer.selector, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))));
}
function _safeTransferFrom(address token, address from, address to, uint256 value) internal {
(bool success, bytes memory data) =
token.call(abi.encodeWithSelector(erc20.transferFrom.selector, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))));
}
}
@storming0x
Copy link

storming0x commented Aug 28, 2021

Do we need to clear/reset elapsed mapping state too in withdraw method?

I see we house clean all other state but not that one, any consequences of not doing it?

I may be reading the code wrong but i understand i can deposit to set the timeElapsed early,
later withdraw and let the time pass and call getReward with my token id and since _getRecipient allows for me to not have my tokenID staked, can i keep earning more rewards than im supposed to earn even without staking?

I see in earn method in line 135 that elapsed.timestamp is used to derive _maxSecondsInside and later calcualte a possible reward,

cannot pin point an exact attack but at first glance could be something to check since it feels fuzzy but I could be wrong.

@storming0x
Copy link

storming0x commented Aug 28, 2021

Any reason for not using EnumerableSet from OZ folks, instead of an array for tokenIds per owner?

tokenIds[msg.sender].push(tokenId);

May save a bit of gas for deleting, etc.

@storming0x
Copy link

storming0x commented Aug 28, 2021

Line 224 if (nftManager.ownerOf(_tokens[i]) == address(this)) {

Not sure when this would not be true, if the address has withdrawn or already exited at this point, it would not have the tokenId in
the _tokens[] array as per house keeping in withdraw.

@storming0x
Copy link

Line 193 wrong event name
emit Deposit(msg.sender, tokenId, _liquidity); should be Withdraw?

@storming0x
Copy link

weird scenario most likely,

If i screw up and direct transfer my tokenId to this contract and later someone calls getReward(lostTokenId) it seems the rewards are transfered back to this contract? is that possible?

Maybe there should be a check that can't send rewards to address(this); else it will be leaking rewards from others.

@storming0x
Copy link

storming0x commented Aug 28, 2021

Does notify need to also do this check done here:
https://github.com/Synthetixio/synthetix/blob/074d63a07e803c947e8828d55142666c28c26f55/contracts/StakingRewards.sol#L123

 // Ensure the provided reward amount is not more than the balance in the contract.
        // This keeps the reward rate in the right range, preventing overflows due to
        // very high values of rewardRate in the earned and rewardsPerToken functions;
        // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
        uint balance = rewardsToken.balanceOf(address(this));
        require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");

@storming0x
Copy link

would airdropping rewards to this contract borked the accounting in any way you can see?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment