diff --git a/contracts/contracts/harvest/Dripper.sol b/contracts/contracts/harvest/Dripper.sol index 02301a7e53..060cd27893 100644 --- a/contracts/contracts/harvest/Dripper.sol +++ b/contracts/contracts/harvest/Dripper.sol @@ -19,10 +19,10 @@ import { IVault } from "../interfaces/IVault.sol"; * * Design notes * - USDT has a smaller resolution than the number of seconds - * in a week, which can make per block payouts have a rounding error. However + * in a week, which can make per second payouts have a rounding error. However * the total effect is not large - cents per day, and this money is * not lost, just distributed in the future. While we could use a higher - * decimal precision for the drip perBlock, we chose simpler code. + * decimal precision for the drip perSecond, we chose simpler code. * - By calculating the changing drip rates on collects only, harvests and yield * events don't have to call anything on this contract or pay any extra gas. * Collect() is already be paying for a single write, since it has to reset @@ -49,7 +49,7 @@ contract Dripper is Governable { struct Drip { uint64 lastCollect; // overflows 262 billion years after the sun dies - uint192 perBlock; // drip rate per block + uint192 perSecond; // drip rate per second } address immutable vault; // OUSD vault @@ -86,7 +86,11 @@ contract Dripper is Governable { /// @dev Change the drip duration. Governor only. /// @param _durationSeconds the number of seconds to drip out the entire /// balance over if no collects were called during that time. - function setDripDuration(uint256 _durationSeconds) external onlyGovernor { + function setDripDuration(uint256 _durationSeconds) + external + virtual + onlyGovernor + { require(_durationSeconds > 0, "duration must be non-zero"); dripDuration = _durationSeconds; _collect(); // duration change take immediate effect @@ -113,21 +117,21 @@ contract Dripper is Governable { returns (uint256) { uint256 elapsed = block.timestamp - _drip.lastCollect; - uint256 allowed = (elapsed * _drip.perBlock); + uint256 allowed = (elapsed * _drip.perSecond); return (allowed > _balance) ? _balance : allowed; } /// @dev Sends the currently dripped funds to be vault, and sets /// the new drip rate based on the new balance. - function _collect() internal { + function _collect() internal virtual { // Calculate send uint256 balance = IERC20(token).balanceOf(address(this)); uint256 amountToSend = _availableFunds(balance, drip); uint256 remaining = balance - amountToSend; - // Calculate new drip perBlock + // Calculate new drip perSecond // Gas savings by setting entire struct at one time drip = Drip({ - perBlock: uint192(remaining / dripDuration), + perSecond: uint192(remaining / dripDuration), lastCollect: uint64(block.timestamp) }); // Send funds diff --git a/contracts/contracts/harvest/FixedRateDripper.sol b/contracts/contracts/harvest/FixedRateDripper.sol index 7178c3ae49..06636e9c29 100644 --- a/contracts/contracts/harvest/FixedRateDripper.sol +++ b/contracts/contracts/harvest/FixedRateDripper.sol @@ -3,121 +3,67 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Governable } from "../governance/Governable.sol"; import { IVault } from "../interfaces/IVault.sol"; +import { Dripper } from "./Dripper.sol"; /** - * @title OUSD Dripper + * @title Fixed Rate Dripper * - * The dripper contract smooths out the yield from point-in-time yield events - * and spreads the yield out over a configurable time period. This ensures a - * continuous per block yield to makes users happy as their next rebase - * amount is always moving up. Also, this makes historical day to day yields - * smooth, rather than going from a near zero day, to a large APY day, then - * back to a near zero day again. - * - * - * Design notes - * - USDT has a smaller resolution than the number of seconds - * in a week, which can make per block payouts have a rounding error. However - * the total effect is not large - cents per day, and this money is - * not lost, just distributed in the future. While we could use a higher - * decimal precision for the drip perBlock, we chose simpler code. - * - By calculating the changing drip rates on collects only, harvests and yield - * events don't have to call anything on this contract or pay any extra gas. - * Collect() is already be paying for a single write, since it has to reset - * the lastCollect time. - * - By having a collectAndRebase method, and having our external systems call - * that, the OUSD vault does not need any changes, not even to know the address - * of the dripper. - * - A rejected design was to retro-calculate the drip rate on each collect, - * based on the balance at the time of the collect. While this would have - * required less state, and would also have made the contract respond more quickly - * to new income, it would break the predictability that is this contract's entire - * purpose. If we did this, the amount of fundsAvailable() would make sharp increases - * when funds were deposited. - * - When the dripper recalculates the rate, it targets spending the balance over - * the duration. This means that every time that collect is called, if no - * new funds have been deposited the duration is being pushed back and the - * rate decreases. This is expected, and ends up following a smoother but - * longer curve the more collect() is called without incoming yield. + * Similar to the Dripper, Fixed Rate Dripper drips out yield per second. + * However the Strategist decides the rate and it doesn't change after + * a drip. * */ -contract FixedRateDripper is Governable { +contract FixedRateDripper is Dripper { using SafeERC20 for IERC20; - address immutable vault; // OUSD vault - address immutable token; // token to drip out - uint256 public dripRate; // WETH per second - uint256 public lastCollect; // Last collect timestamp + event DripRateUpdated(uint192 oldDripRate, uint192 newDripRate); - constructor(address _vault, address _token) { - vault = _vault; - token = _token; + /** + * @dev Verifies that the caller is the Governor or Strategist. + */ + modifier onlyGovernorOrStrategist() { + require( + isGovernor() || msg.sender == IVault(vault).strategistAddr(), + "Caller is not the Strategist or Governor" + ); + _; } - /// @notice How much funds have dripped out already and are currently - // available to be sent to the vault. - /// @return The amount that would be sent if a collect was called - function availableFunds() external view returns (uint256) { - uint256 balance = IERC20(token).balanceOf(address(this)); - return _availableFunds(balance); - } + constructor(address _vault, address _token) Dripper(_vault, _token) {} - /// @notice Collect all dripped funds and send to vault. - /// Recalculate new drip rate. - function collect() external { - _collect(); + /// @inheritdoc Dripper + function setDripDuration(uint256) external virtual override { + // Not used in FixedRateDripper + revert("Drip duration disabled"); } - /// @notice Collect all dripped funds, send to vault, recalculate new drip - /// rate, and rebase OUSD. - function collectAndRebase() external { - _collect(); - IVault(vault).rebase(); - } + /// @inheritdoc Dripper + function _collect() internal virtual override { + // Calculate amount to send + uint256 balance = IERC20(token).balanceOf(address(this)); + uint256 amountToSend = _availableFunds(balance, drip); - /// @dev Change the drip rate. Governor only. - /// @param _rate Amount of token to drip per second - /// balance over if no collects were called during that time. - function setDripRate(uint256 _rate) external onlyGovernor { - require(_rate > 0, "rate must be non-zero"); - // Collect at current rate - _collect(); - // Update the rate - dripRate = _rate; - } + // Update timestamp + drip.lastCollect = uint64(block.timestamp); - /// @dev Transfer out ERC20 tokens held by the contract. Governor only. - /// @param _asset ERC20 token address - /// @param _amount amount to transfer - function transferToken(address _asset, uint256 _amount) - external - onlyGovernor - { - IERC20(_asset).safeTransfer(governor(), _amount); + // Send funds + IERC20(token).safeTransfer(vault, amountToSend); } - /// @dev Calculate available funds by taking the lower of either the - /// currently dripped out funds or the balance available. - /// Uses passed in parameters to calculate with for gas savings. - /// @param _balance current balance in contract - function _availableFunds(uint256 _balance) internal view returns (uint256) { - uint256 elapsed = block.timestamp - lastCollect; - uint256 allowed = (elapsed * dripRate); - return (allowed > _balance) ? _balance : allowed; - } + /** + * @dev Sets the drip rate. Callable by Strategist or Governor. + * Can be set to zero to stop dripper. + * @param _perSecond Rate of WETH to drip per second + */ + function setDripRate(uint192 _perSecond) external onlyGovernorOrStrategist { + emit DripRateUpdated(_perSecond, drip.perSecond); - /// @dev Sends the currently dripped funds to be vault, and sets - /// the new drip rate based on the new balance. - function _collect() internal { - // Calculate send - uint256 balance = IERC20(token).balanceOf(address(this)); - uint256 amountToSend = _availableFunds(balance); - lastCollect = block.timestamp; + // Collect at existing rate + _collect(); - // Send funds - IERC20(token).safeTransfer(vault, amountToSend); + // Update rate + drip.perSecond = _perSecond; } } diff --git a/contracts/deploy/base/013_fixed_rate_dripper.js b/contracts/deploy/base/013_fixed_rate_dripper.js new file mode 100644 index 0000000000..ca2e4aabc3 --- /dev/null +++ b/contracts/deploy/base/013_fixed_rate_dripper.js @@ -0,0 +1,30 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { deployWithConfirmation } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "013_fixed_rate_dripper", + }, + async ({ ethers }) => { + const cOETHbDripperProxy = await ethers.getContract("OETHBaseDripperProxy"); + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + + // Deploy new implementation + const dOETHbDripper = await deployWithConfirmation("FixedRateDripper", [ + cOETHbVaultProxy.address, + addresses.base.WETH, + ]); + + return { + actions: [ + { + // 1. Upgrade Dripper + contract: cOETHbDripperProxy, + signature: "upgradeTo(address)", + args: [dOETHbDripper.address], + }, + ], + }; + } +); diff --git a/contracts/docs/DripperStorage.svg b/contracts/docs/DripperStorage.svg index 9ee1fef514..de6ac6b239 100644 --- a/contracts/docs/DripperStorage.svg +++ b/contracts/docs/DripperStorage.svg @@ -39,7 +39,7 @@ type: variable (bytes) -uint192: perBlock (24) +uint192: perSecond (24) uint64: lastCollect (8) diff --git a/contracts/docs/OETHDripperStorage.svg b/contracts/docs/OETHDripperStorage.svg index a6b80c2e8b..e2da1f7190 100644 --- a/contracts/docs/OETHDripperStorage.svg +++ b/contracts/docs/OETHDripperStorage.svg @@ -39,7 +39,7 @@ type: variable (bytes) -uint192: perBlock (24) +uint192: perSecond (24) uint64: lastCollect (8)