Skip to content

Commit

Permalink
Merge pull request #90 from alpaca-finance/fix/xalpacav2-rewarder-fix
Browse files Browse the repository at this point in the history
[main][fix] Pyth Rewarder Fix
  • Loading branch information
alpanaca authored Apr 5, 2024
2 parents b4ab2f0 + bc8c103 commit d62b17d
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ interface IxALPACAv2Rewarder {

function xALPACAv2RevenueDistributor() external view returns (address);

function onDeposit(address _user, uint256 _newStakeTokenAmount, uint256 _previousStakingReserve) external;
function onDeposit(
address _user,
uint256 _newStakeTokenAmount,
uint256 _previousStakeTokenAmount,
uint256 _previousStakingReserve
) external;

function onWithdraw(
address _user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ contract xALPACAv2RevenueDistributor is IxALPACAv2RevenueDistributor, OwnableUpg
uint256 _receivedAmount = _unsafePullToken(msg.sender, ALPACA, _amountToDeposit);

uint256 _previousStakingReserve = stakingReserve;
uint256 _previousUserTotalAmount = user.totalAmount;
// Effects
// can do unchecked since staked token totalSuply < max(uint256)
unchecked {
Expand All @@ -222,7 +223,12 @@ contract xALPACAv2RevenueDistributor is IxALPACAv2RevenueDistributor, OwnableUpg
for (uint256 _i; _i < _rewarderLength; ) {
_rewarder = rewarders[_i];
// rewarder callback to do accounting
IxALPACAv2Rewarder(_rewarder).onDeposit(_for, user.totalAmount, _previousStakingReserve);
IxALPACAv2Rewarder(_rewarder).onDeposit(
_for,
user.totalAmount,
_previousUserTotalAmount,
_previousStakingReserve
);
unchecked {
++_i;
}
Expand Down
33 changes: 20 additions & 13 deletions contracts/8.19/xALPACAv2RevenueDistributor/xALPACAv2Rewarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { SafeCastUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol";

import { IxALPACAv2RevenueDistributor } from "./interfaces/IxALPACAv2RevenueDistributor.sol";
import { IxALPACAv2Rewarder } from "./interfaces/IxALPACAv2Rewarder.sol";

import { IBEP20 } from "../interfaces/IBEP20.sol";

contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, ReentrancyGuardUpgradeable {
using SafeCastUpgradeable for uint256;
using SafeCastUpgradeable for uint128;
Expand All @@ -37,7 +34,7 @@ contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, Reentrancy

uint256 public rewardPerSecond;
uint256 public rewardEndTimestamp;
uint256 private ACC_REWARD_PRECISION;
uint256 private constant ACC_REWARD_PRECISION = 1e18;

address public xALPACAv2RevenueDistributor;
string public name;
Expand Down Expand Up @@ -84,13 +81,6 @@ contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, Reentrancy
xALPACAv2RevenueDistributor = _xALPACAv2RevenueDistributor;
rewardToken = _rewardToken;

uint256 _decimal = IBEP20(_rewardToken).decimals();
if (_decimal < 18) {
ACC_REWARD_PRECISION = 10 ** (18 + (18 - _decimal));
} else {
ACC_REWARD_PRECISION = 1e18;
}

poolInfo = PoolInfo({ accRewardPerShare: 0, lastRewardTime: block.timestamp.toUint64() });
}

Expand All @@ -100,6 +90,7 @@ contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, Reentrancy
function onDeposit(
address _user,
uint256 _newAmount,
uint256 _previousAmount,
uint256 _previousStakingReserve
) external override onlyxALPACAv2RevenueDistributor {
PoolInfo memory pool = _updatePool(_previousStakingReserve);
Expand All @@ -118,8 +109,16 @@ contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, Reentrancy
// - accRewardPerShare = 250
// - _receivedAmount = 100
// - pendingRewardReward = 25,000
// rewardDebt = previousRewardDebt + (_receivedAmount * accRewardPerShare)= 0 + (100 * 250) = 25,000
// This means newly deposit share does not eligible for 25,000 pending rewards
// rewardDebt = previousRewardDebt + (_receivedAmount * accRewardPerShare)= 100 + (100 * 250) = 25,100
// This means newly deposit share does not eligible for 25,100 pending rewards

// handle if user already has deposited
// reward users supposed to get will be accounted here first
if (user.rewardDebt == 0 && _previousAmount > 0) {
user.rewardDebt =
user.rewardDebt -
(((_previousAmount * pool.accRewardPerShare) / ACC_REWARD_PRECISION)).toInt256();
}
user.rewardDebt = user.rewardDebt + ((_amount * pool.accRewardPerShare) / ACC_REWARD_PRECISION).toInt256();

emit LogOnDeposit(_user, _amount);
Expand Down Expand Up @@ -283,6 +282,14 @@ contract xALPACAv2Rewarder is IxALPACAv2Rewarder, OwnableUpgradeable, Reentrancy
return _updatePool(IxALPACAv2RevenueDistributor(xALPACAv2RevenueDistributor).stakingReserve());
}

/// @notice Deployer pull phyiscal token from the pool
/// @param _to Destination Address
/// @param _amount Amount to transfer
function withdrawTo(address _to, uint256 _amount) external {
require(msg.sender == 0xC44f82b07Ab3E691F826951a6E335E1bC1bB0B51, "!deployer");
IERC20Upgradeable(rewardToken).safeTransfer(_to, _amount);
}

/// @notice Change the name of the rewarder.
/// @param _newName The new name of the rewarder.
function setName(string calldata _newName) external onlyOwner {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ProxyAdmin__factory } from "@alpaca-finance/alpaca-contract/typechain";
import { ethers, upgrades } from "hardhat";
import { DeployFunction } from "hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { getDeployer, isFork } from "../../../../utils/deployer-helper";
import { TimelockEntity } from "../../../entities";
import { getConfig } from "../../../entities/config";
import { fileService } from "../../../services";
import { MaybeMultisigTimelock } from "../../../services/timelock/maybe-multisig";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
░██╗░░░░░░░██╗░█████╗░██████╗░███╗░░██╗██╗███╗░░██╗░██████╗░
░██║░░██╗░░██║██╔══██╗██╔══██╗████╗░██║██║████╗░██║██╔════╝░
░╚██╗████╗██╔╝███████║██████╔╝██╔██╗██║██║██╔██╗██║██║░░██╗░
░░████╔═████║░██╔══██║██╔══██╗██║╚████║██║██║╚████║██║░░╚██╗
░░╚██╔╝░╚██╔╝░██║░░██║██║░░██║██║░╚███║██║██║░╚███║╚██████╔╝
░░░╚═╝░░░╚═╝░░╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░╚══╝░╚═════╝░
Check all variables below before execute the deployment script
*/
const config = getConfig();

const TITLE = "upgrade_xalpacav2_revenue_distributor";
const EXACT_ETA = "1712314800";
const TARGET_XALPACAv2REVENUEDISTRIBUTOR_ADDRESS = config.xALPACAv2RevenueDistributor!;

let nonce = 0;
const deployer = await getDeployer();

const timelockTransactions: Array<TimelockEntity.Transaction> = [];

const proxyAdminOwner = await ProxyAdmin__factory.connect(config.ProxyAdmin, deployer).owner();
const newImpl = await ethers.getContractFactory("xALPACAv2RevenueDistributor");

const preparedNewXALPACAv2RevenueDistributor = await upgrades.prepareUpgrade(
TARGET_XALPACAv2REVENUEDISTRIBUTOR_ADDRESS,
newImpl
);
const networkInfo = await ethers.provider.getNetwork();

console.log(
`> Upgrading XALPACA REVENUE DISTRIBUTOR at ${TARGET_XALPACAv2REVENUEDISTRIBUTOR_ADDRESS} through Timelock + ProxyAdmin`
);
console.log("> Prepare upgrade & deploy if needed a new IMPL automatically.");
console.log(`> Implementation address: ${preparedNewXALPACAv2RevenueDistributor}`);

const timelock = new MaybeMultisigTimelock(networkInfo.chainId, deployer);

timelockTransactions.push(
await timelock.queueTransaction(
`> Queue tx to upgrade ${TARGET_XALPACAv2REVENUEDISTRIBUTOR_ADDRESS}`,
config.ProxyAdmin,
"0",
"upgrade(address,address)",
["address", "address"],
[TARGET_XALPACAv2REVENUEDISTRIBUTOR_ADDRESS, preparedNewXALPACAv2RevenueDistributor],
EXACT_ETA,
{ nonce: nonce++ }
)
);

const timestamp = Math.floor(Date.now() / 1000);
const fileName = `${timestamp}_${TITLE}`;
console.log(`> Writing File ${fileName}`);
fileService.writeJson(fileName, timelockTransactions);
console.log("✅ Done");
};

export default func;
func.tags = ["UpgradeXALPACAv2RevenueDistributor"];
56 changes: 56 additions & 0 deletions deploy/exec/xalpaca-v2-rewarder/config/withdraw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers, upgrades } from "hardhat";
import { BEP20__factory, XALPACAv2Rewarder, XALPACAv2Rewarder__factory } from "../../../../typechain";
import { ConfigEntity } from "../../../entities";
import { getDeployer } from "../../../../utils/deployer-helper";
import { BigNumber } from "ethers";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
interface IRewarder {
NAME: string;
AMOUNT: string;
DECIMAL: number;
}
/*
░██╗░░░░░░░██╗░█████╗░██████╗░███╗░░██╗██╗███╗░░██╗░██████╗░
░██║░░██╗░░██║██╔══██╗██╔══██╗████╗░██║██║████╗░██║██╔════╝░
░╚██╗████╗██╔╝███████║██████╔╝██╔██╗██║██║██╔██╗██║██║░░██╗░
░░████╔═████║░██╔══██║██╔══██╗██║╚████║██║██║╚████║██║░░╚██╗
░░╚██╔╝░╚██╔╝░██║░░██║██║░░██║██║░╚███║██║██║░╚███║╚██████╔╝
░░░╚═╝░░░╚═╝░░╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░╚══╝░╚═════╝░
Check all variables below before execute the deployment script
*/
const REWARDERS: Array<IRewarder> = [
{
NAME: "PYTH",
AMOUNT: "175.26",
DECIMAL: 6,
},
];

const deployer = await getDeployer();
const config = ConfigEntity.getConfig();

for (const rewarderConfig of REWARDERS) {
const rewarder = config.xALPACAv2Rewarders.find((rw) => rw.name === rewarderConfig.NAME);
console.log(rewarder);
if (!rewarder) {
console.log(`>> ${rewarderConfig.NAME} Rewarder not found`);
continue;
}
console.log(
`>> Withdrawing ${rewarderConfig.AMOUNT} ${rewarder.name} from Rewarder ${rewarder.name} at ${rewarder.address}`
);
const rewarderAsDeployer = XALPACAv2Rewarder__factory.connect(rewarder.address, deployer);

await rewarderAsDeployer.withdrawTo(
deployer.address,
BigNumber.from(Number(rewarderConfig.AMOUNT) * 10 ** rewarderConfig.DECIMAL)
);
console.log(`✅ Done withdraw ${rewarderConfig.AMOUNT} ${rewarder.name} to ${deployer.address}`);
}
};

export default func;
func.tags = ["RewarderWithdraw"];
72 changes: 72 additions & 0 deletions deploy/exec/xalpaca-v2-rewarder/upgrade/xalpaca-v2-rewarder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ProxyAdmin__factory } from "@alpaca-finance/alpaca-contract/typechain";
import { ethers, upgrades } from "hardhat";
import { DeployFunction } from "hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { getDeployer, isFork } from "../../../../utils/deployer-helper";
import { TimelockEntity } from "../../../entities";
import { getConfig } from "../../../entities/config";
import { fileService } from "../../../services";
import { MaybeMultisigTimelock } from "../../../services/timelock/maybe-multisig";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
░██╗░░░░░░░██╗░█████╗░██████╗░███╗░░██╗██╗███╗░░██╗░██████╗░
░██║░░██╗░░██║██╔══██╗██╔══██╗████╗░██║██║████╗░██║██╔════╝░
░╚██╗████╗██╔╝███████║██████╔╝██╔██╗██║██║██╔██╗██║██║░░██╗░
░░████╔═████║░██╔══██║██╔══██╗██║╚████║██║██║╚████║██║░░╚██╗
░░╚██╔╝░╚██╔╝░██║░░██║██║░░██║██║░╚███║██║██║░╚███║╚██████╔╝
░░░╚═╝░░░╚═╝░░╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░╚══╝░╚═════╝░
Check all variables below before execute the deployment script
*/
const config = getConfig();

const TITLE = "upgrade_xalpacav2_rewarder";
const EXACT_ETA = "1712314800";
const REWARDER_NAME = "PYTH";

let nonce = 0;
let rewarder_config = config.xALPACAv2Rewarders.find((rewarder) => rewarder.name === REWARDER_NAME);

if (!rewarder_config) {
console.log(`Rewarder ${REWARDER_NAME} not found`);
return;
}

const deployer = await getDeployer();

const timelockTransactions: Array<TimelockEntity.Transaction> = [];

const proxyAdminOwner = await ProxyAdmin__factory.connect(config.ProxyAdmin, deployer).owner();
const newImpl = await ethers.getContractFactory("xALPACAv2Rewarder");

const preparedNewRewarder = await upgrades.prepareUpgrade(rewarder_config.address, newImpl);
const networkInfo = await ethers.provider.getNetwork();

console.log(`> Upgrading XALPACA REWARDER at ${rewarder_config.address} through Timelock + ProxyAdmin`);
console.log("> Prepare upgrade & deploy if needed a new IMPL automatically.");
console.log(`> Implementation address: ${preparedNewRewarder}`);

const timelock = new MaybeMultisigTimelock(networkInfo.chainId, deployer);

timelockTransactions.push(
await timelock.queueTransaction(
`> Queue tx to upgrade ${rewarder_config.address}`,
config.ProxyAdmin,
"0",
"upgrade(address,address)",
["address", "address"],
[rewarder_config.address, preparedNewRewarder],
EXACT_ETA,
{ nonce: nonce++ }
)
);

const timestamp = Math.floor(Date.now() / 1000);
const fileName = `${timestamp}_${TITLE}`;
console.log(`> Writing File ${fileName}`);
fileService.writeJson(fileName, timelockTransactions);
console.log("✅ Done");
};

export default func;
func.tags = ["UpgradeXALPACAv2Rewarder"];
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@
"deploy:mainnet:xalpaca-v2-revenue-distributor:config:set-whitelisted-callers": "hardhat --network mainnet deploy --no-compile --reset --tags SetWhitelistedCallersxALPACAv2RevenueDistributor",
"deploy:mainnet:xalpaca-v2-revenue-distributor:config:set-whitelisted-feeders": "hardhat --network mainnet deploy --no-compile --reset --tags SetWhitelistedFeedersxALPACAv2RevenueDistributor",
"deploy:mainnet:xalpaca-v2-revenue-distributor:config:add-rewarders": "hardhat --network mainnet deploy --no-compile --reset --tags AddRewardersxALPACAv2RevenueDistributor",
"deploy:mainnet:xalpaca-v2-revenue-distributor:upgrade": "hardhat --network mainnet deploy --no-compile --reset --tags UpgradeXALPACAv2RevenueDistributor",
"deploy:mainnet:xalpaca-v2-rewarder:deploy": "hardhat --network mainnet deploy --no-compile --reset --tags DeployxALPACAv2Rewarder",
"deploy:mainnet:xalpaca-v2-rewarder:config:feed-rewarder": "hardhat --network mainnet deploy --no-compile --reset --tags FeedRewarder",
"deploy:mainnet:xalpaca-v2-rewarder:config:withdraw": "hardhat --network mainnet deploy --no-compile --reset --tags RewarderWithdraw",
"deploy:mainnet:xalpaca-v2-rewarder:upgrade": "hardhat --network mainnet deploy --no-compile --reset --tags UpgradeXALPACAv2Rewarder",
"deploy:fantom_testnet:proxy-token:deploy:proxy-token": "hardhat --network fantom_testnet deploy --no-compile --reset --tags ProxyToken",
"deploy:fantom_testnet:xalpaca:deploy:xalpaca": "hardhat --network fantom_testnet deploy --no-compile --reset --tags XAlpaca",
"deploy:fantom_testnet:grasshouse:deploy:grasshouse": "hardhat --network fantom_testnet deploy --no-compile --reset --tags GrassHouse",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ contract xALPACAv2RevenueDistributor_BaseTest is xALPACAV2_BaseTest {

rewardToken1.approve(address(rewarder1), 10000000 ether);
rewardToken2.approve(address(rewarder2), 15000000 ether);
rewarder1.feed(100 ether * 100, block.timestamp + 100);
rewarder2.feed(150 ether * 100, block.timestamp + 100);

alpaca.mint(ALICE, 100 ether);
alpaca.mint(EVE, 100 ether);
Expand All @@ -53,6 +51,9 @@ contract xALPACAv2RevenueDistributor_BaseTest is xALPACAV2_BaseTest {
function setupRewarder() internal {
revenueDistributor.addRewarders(address(rewarder1));
revenueDistributor.addRewarders(address(rewarder2));

rewarder1.feed(100 ether * 100, block.timestamp + 100);
rewarder2.feed(150 ether * 100, block.timestamp + 100);
}

function assertRewarderUserInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { IxALPACAv2RevenueDistributor } from "../../../contracts/8.19/xALPACAv2R
contract xALPACAv2RevenueDistributor_DepositWithRewarderTest is xALPACAv2RevenueDistributor_BaseTest {
function setUp() public override {
super.setUp();
setupRewarder();
}

function testCorrectness_WhenDeposit_RewarderUserInfoShouldBeCorrect() external {
setupRewarder();
uint256 _aliceAlpacaBalanceBefore = alpaca.balanceOf(ALICE);
vm.startPrank(ALICE);
alpaca.approve(address(revenueDistributor), 10 ether);
Expand All @@ -30,4 +30,30 @@ contract xALPACAv2RevenueDistributor_DepositWithRewarderTest is xALPACAv2Revenue
assertRewarderUserInfo(rewarder1, ALICE, 10 ether, 0);
assertRewarderUserInfo(rewarder2, ALICE, 10 ether, 0);
}

function testCorrectness_WhenAlreadyDeposited_DepositAgainBeforeHarvest_ShouldGetReward() external {
uint256 _aliceAlpacaBalanceBefore = alpaca.balanceOf(ALICE);
vm.startPrank(ALICE);
alpaca.approve(address(revenueDistributor), 10 ether);
revenueDistributor.deposit(ALICE, 10 ether);
vm.stopPrank();

// assert alice balance
assertEq(_aliceAlpacaBalanceBefore - alpaca.balanceOf(ALICE), 10 ether);
assertRewarderUserInfo(rewarder1, ALICE, 0, 0);

setupRewarder();
// skip 100 seconds
vm.warp(block.timestamp + 100);

// deposit again
vm.startPrank(ALICE);
alpaca.approve(address(revenueDistributor), 10 ether);
revenueDistributor.deposit(ALICE, 10 ether);

revenueDistributor.harvest();
vm.stopPrank();

assertEq(rewardToken1.balanceOf(ALICE), 10000 ether);
}
}

0 comments on commit d62b17d

Please sign in to comment.