-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WOETH - Fixed yield rate each day #2421
Open
DanielVF
wants to merge
39
commits into
sparrowDom/woeth_hack_proof
Choose a base branch
from
DanielVF/fixedDayYield
base: sparrowDom/woeth_hack_proof
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
7db7545
Initial concept
DanielVF e7b5daa
Remove reverts from startYield
DanielVF c400f5f
Remove unneeded import
DanielVF 0054c26
Add yield event, deduplicate startYield()
DanielVF 1606ac6
Tighten startYield()
DanielVF 690e81c
higher daily cap. Still way under safety reasons
DanielVF 5ffec31
There should be no yield on the block that changes happen
DanielVF 441beb6
There's something to compiling
DanielVF 2661656
Allow for negative hardAssets
DanielVF 9edd73b
Formatting cleanup
DanielVF 897acc4
Remove rate
DanielVF 79ab2d7
enhanced yield testing
DanielVF 6c0b17e
add safecasts
sparrowDom d362ea8
remove comment
sparrowDom bea7f4f
go to uint128
DanielVF 779e8e5
nicer ordering
DanielVF 8eebe1a
remove parenthesis
sparrowDom 9ae5a3e
make vars local
sparrowDom 49be3dd
nicer for prettier
DanielVF 72f382b
Cleaner elapsed calc
DanielVF f01c0b2
Cleaner to user IERC20 when not a special OETH need
DanielVF 554570c
Better error message means no override needed
DanielVF dc5473e
Remove unneeded import
DanielVF 6b95b0c
Remove unneeded using
DanielVF 8790f72
Recalc in transfers
DanielVF 3a953ea
Merge remote-tracking branch 'origin/sparrowDom/woeth_hack_proof' int…
sparrowDom b2ea11c
correct bad merge mistakes
sparrowDom 793e37a
add tests for yield start events on deposit mint redeem withdraw
sparrowDom 62fceca
Tighten pass
DanielVF c2434c2
fix compile error
sparrowDom ab81400
fix issue when a yield period should start
sparrowDom 404f407
Correct behavior, lock in no yield for usual period
DanielVF cacb9db
Better name, scheduleYield
DanielVF a15851d
Better name, userAssets
DanielVF e422a5e
Update comments
DanielVF add45c7
_min to pure
DanielVF e9801e4
Begin readme on wOETH
DanielVF 78050ef
The Rappie Simplification
DanielVF a53a8ce
readme update
DanielVF File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# WOETH token | ||
|
||
An ERC4626 contract that wraps a rebasing token and allows it to be treated as a non-rebasing value accrual token. | ||
|
||
This contract distributes yield slowly over 23 hour periods. This prevents donation attacks against borrowing on lending platforms. | ||
|
||
It is designed to work only with up-only rebasing tokens. The asset token must not make reenterable external calls on transfers. | ||
|
||
We plan on minting at least 1e14 wOETH tokens on a new deploy and sending them to a dead address before any user funds are deposited. | ||
|
||
## Invariants | ||
|
||
### Yield timing | ||
|
||
Yield is distributed evenly per second from the start of a yield period at (`scheduleYield()` + 1) to the end of the period, inclusive. | ||
|
||
An example of a three second yield period. | ||
|
||
[ ~ ][Yield][Yield][Yield][ ][ ] | ||
[schedule][ ][ ][ end ][ ][ ] | ||
|
||
Yield only can happen at the start of a block, and at most once per block. As a result: | ||
|
||
> transfers do not change `totalAssets()` | ||
|
||
> scheduleYield() does not change `totalAssets()` | ||
|
||
> all other actions do not change `totalAssets()`, beyond the OETH transferred to or from the user | ||
|
||
Donations are slowed, and have no effect on the current block. Given that this contract only works off of balances, there is no difference from wOETH's point of view between an OETH donation, and an OETH rebase. | ||
|
||
> sending OETH to the contract, or positive rebasing from OETH, will not change `totalAssets()`. | ||
|
||
Yield is evenly spread. | ||
|
||
> yield given in a second, when yield is active, will be (yieldAssets / YIELD_TIME) either rounded down or rounded up. | ||
|
||
> yield given in a second, when the block timestamp is past the end date, will be 0 | ||
|
||
Because we operate on blockchains with many different block times, the per block yields may vary depending on how many active yield seconds elapse in each. | ||
|
||
### Solvency | ||
|
||
The protocol rounds against the user, in favor of the protocol | ||
|
||
> Any series of actions by a single user in a single block, will not result in an increase in their (OETH + previewRedeem(balance))[^1] | ||
|
||
> At any time, all users of the system can redeem all their wOETH | ||
|
||
> The actual redeem amounts will match previewRedeem()[^1] | ||
|
||
> `previewRedeem()` will never go down, outside of an external loss of OETH balance | ||
|
||
> The `trackedAssets` will never exceed wOETH's balance of OETH, outside of an external loss of OETH balance | ||
|
||
> The sum of `totalAssets()` will never exceed wOETH's balance of OETH, outside of an external loss of OETH balance | ||
|
||
> `trackedAssets - yieldAssets` will never be negative | ||
|
||
### 4626-ness | ||
|
||
This is an ERC4626, as such it should follow correct behaviors for an ERC4626. | ||
|
||
|
||
[^1] When OETH has sufficient transfer resolution. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,31 +6,41 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; | ||
|
||
import { StableMath } from "../utils/StableMath.sol"; | ||
import { Governable } from "../governance/Governable.sol"; | ||
import { Initializable } from "../utils/Initializable.sol"; | ||
import { OETH } from "./OETH.sol"; | ||
|
||
/** | ||
* @title OETH Token Contract | ||
* @author Origin Protocol Inc | ||
* @author Origin Protocol | ||
* | ||
* @dev An important capability of this contract is that it isn't susceptible to changes of the | ||
* exchange rate of WOETH/OETH if/when someone sends the underlying asset (OETH) to the contract. | ||
* If OETH weren't rebasing this could be achieved by solely tracking the ERC20 transfers of the OETH | ||
* token on mint, deposit, redeem, withdraw. The issue is that OETH is rebasing and OETH balances | ||
* will change when the token rebases. For that reason we are tracking the WOETH contract credits and | ||
* credits per token in those 4 actions. That way WOETH can keep an accurate track of the OETH balance | ||
* ignoring any unexpected transfers of OETH to this contract. | ||
* @dev An ERC4626 contract that wraps a rebasing token | ||
* and allows it to be treated as a non-rebasing value accrual token. | ||
* This contract distributes yield slowly over 23 hours which prevents | ||
* donation attacks against lending platforms. | ||
* It is designed to work only with up-only rebasing tokens. | ||
* The asset token must not make reenterable external calls on transfers. | ||
*/ | ||
|
||
contract WOETH is ERC4626, Governable, Initializable { | ||
using SafeERC20 for IERC20; | ||
using StableMath for uint256; | ||
uint256 public oethCreditsHighres; | ||
bool private _oethCreditsInitialized; | ||
uint256[48] private __gap; | ||
using SafeCast for uint256; | ||
|
||
uint256 public trackedAssets; | ||
uint128 public yieldAssets; | ||
uint64 public yieldEnd; | ||
bool private _initialized2; | ||
uint256[47] private __gap; | ||
|
||
uint256 public constant YIELD_TIME = 1 days - 1 hours; | ||
|
||
event YiedPeriodStarted( | ||
uint256 trackedAssets, | ||
uint256 yieldAssets, | ||
uint256 yieldEnd | ||
); | ||
|
||
// no need to set ERC20 name and symbol since they are overridden in WOETH & WOETHBase | ||
constructor(ERC20 underlying_) | ||
|
@@ -49,23 +59,14 @@ contract WOETH is ERC4626, Governable, Initializable { | |
} | ||
|
||
/** | ||
* @notice secondary initializer that newly deployed contracts will execute as part | ||
* of primary initialize function and the existing contracts will have it called | ||
* as a governance operation. | ||
* @notice Upgrade contract to support yield periods. | ||
* Called automatically on new contracts via initialize() | ||
*/ | ||
function initialize2() public onlyGovernor { | ||
require(!_oethCreditsInitialized, "Initialize2 already called"); | ||
|
||
_oethCreditsInitialized = true; | ||
/* | ||
* This contract is using creditsBalanceOfHighres rather than creditsBalanceOf since this | ||
* ensures better accuracy when rounding. Also creditsBalanceOf can be a little | ||
* finicky since it reports Highres version of credits and creditsPerToken | ||
* when the account is a fresh one. That doesn't have an effect on mainnet since | ||
* WOETH has already seen transactions. But it is rather annoying in unit test | ||
* environment. | ||
*/ | ||
oethCreditsHighres = _getOETHCredits(); | ||
require(!_initialized2, "Initialize2 already called"); | ||
_initialized2 = true; | ||
DanielVF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
trackedAssets = IERC20(asset()).balanceOf(address(this)); | ||
} | ||
|
||
function name() | ||
|
@@ -90,103 +91,114 @@ contract WOETH is ERC4626, Governable, Initializable { | |
|
||
/** | ||
* @notice Transfer token to governor. Intended for recovering tokens stuck in | ||
* contract, i.e. mistaken sends. Cannot transfer OETH | ||
* contract, i.e. mistaken sends. Cannot transfer the core asset | ||
* @param asset_ Address for the asset | ||
* @param amount_ Amount of the asset to transfer | ||
*/ | ||
function transferToken(address asset_, uint256 amount_) | ||
external | ||
onlyGovernor | ||
{ | ||
//@dev TODO: we could implement a feature where if anyone sends OETH directly to | ||
// the contract, that we can let the governor transfer the excess of the token. | ||
require(asset_ != address(asset()), "Cannot collect OETH"); | ||
require(asset_ != address(asset()), "Cannot collect core asset"); | ||
IERC20(asset_).safeTransfer(governor(), amount_); | ||
} | ||
|
||
/** @dev See {IERC4262-totalAssets} */ | ||
function totalAssets() public view override returns (uint256) { | ||
uint256 creditsPerTokenHighres = OETH(asset()) | ||
.rebasingCreditsPerTokenHighres(); | ||
|
||
return (oethCreditsHighres).divPrecisely(creditsPerTokenHighres); | ||
/* @notice Start the next yield period, if one is not active. | ||
* New yield will not start until time has moved forward. | ||
*/ | ||
function scheduleYield() public { | ||
// If we are currently in a yield period, keep everything the same | ||
if (block.timestamp < yieldEnd) { | ||
return; | ||
} | ||
|
||
// Read current assets | ||
DanielVF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
uint256 _computedAssets = totalAssets(); | ||
uint256 _actualAssets = IERC20(asset()).balanceOf(address(this)); | ||
|
||
// Compute next yield period values | ||
if (_actualAssets <= _computedAssets) { | ||
yieldAssets = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Should ensure we have a test for a loss of balance case. |
||
trackedAssets = _actualAssets; | ||
} else if (_actualAssets > _computedAssets) { | ||
uint256 _newYield = _actualAssets - _computedAssets; | ||
uint256 _maxYield = (_computedAssets * 5) / 100; // Maximum of 5% increase in assets per day | ||
_newYield = _min(_min(_newYield, _maxYield), type(uint128).max); | ||
yieldAssets = _newYield.toUint128(); | ||
trackedAssets = _computedAssets + yieldAssets; | ||
} | ||
// raw cast is deliberate, since this will not perma revert | ||
yieldEnd = uint64(block.timestamp + YIELD_TIME); | ||
emit YiedPeriodStarted(trackedAssets, yieldAssets, yieldEnd); | ||
} | ||
|
||
function _getOETHCredits() | ||
internal | ||
view | ||
returns (uint256 oethCreditsHighres) | ||
{ | ||
(oethCreditsHighres, , ) = OETH(asset()).creditsBalanceOfHighres( | ||
address(this) | ||
); | ||
/** | ||
* @notice Returns the assets currently backing the total supply. | ||
* Does not include future yield held that will stream per block. | ||
* @return totalAssets() | ||
*/ | ||
function totalAssets() public view override returns (uint256) { | ||
uint256 _end = yieldEnd; | ||
if (block.timestamp >= _end) { | ||
return trackedAssets; | ||
} else if (block.timestamp <= _end - YIELD_TIME) { | ||
return trackedAssets - yieldAssets; | ||
} | ||
uint256 elapsed = (block.timestamp + YIELD_TIME) - _end; | ||
uint256 _unlockedYield = (yieldAssets * elapsed) / YIELD_TIME; | ||
return trackedAssets + _unlockedYield - yieldAssets; | ||
} | ||
|
||
/** @dev See {IERC4262-deposit} */ | ||
function deposit(uint256 oethAmount, address receiver) | ||
public | ||
override | ||
returns (uint256 woethAmount) | ||
{ | ||
if (oethAmount == 0) return 0; | ||
|
||
/** | ||
* Initially we attempted to do the credits calculation within this contract and try | ||
* to mimic OUSD's implementation. This way 1 external call less would be required. Due | ||
* to a different way OUSD is calculating credits: | ||
* - always rounds credits up | ||
* - operates on final user balances before converting to credits | ||
* - doesn't perform additive / subtractive calculation with credits once they are converted | ||
* from balances | ||
* | ||
* We've decided that it is safer to read the credits diff directly from the OUSD contract | ||
* and not face the risk of a compounding error in oethCreditsHighres that could result in | ||
* inaccurate `convertToShares` & `convertToAssets` which consequently would result in faulty | ||
* `previewMint` & `previewRedeem`. High enough error can result in different conversion rates | ||
* which a flash loan entering via `deposit` and exiting via `redeem` (or entering via `mint` | ||
* and exiting via `withdraw`) could take advantage of. | ||
*/ | ||
uint256 creditsBefore = _getOETHCredits(); | ||
woethAmount = super.deposit(oethAmount, receiver); | ||
oethCreditsHighres += _getOETHCredits() - creditsBefore; | ||
trackedAssets += oethAmount; | ||
scheduleYield(); | ||
} | ||
|
||
/** @dev See {IERC4262-mint} */ | ||
function mint(uint256 woethAmount, address receiver) | ||
public | ||
override | ||
returns (uint256 oethAmount) | ||
{ | ||
if (woethAmount == 0) return 0; | ||
|
||
uint256 creditsBefore = _getOETHCredits(); | ||
oethAmount = super.mint(woethAmount, receiver); | ||
oethCreditsHighres += _getOETHCredits() - creditsBefore; | ||
trackedAssets += oethAmount; | ||
scheduleYield(); | ||
} | ||
|
||
/** @dev See {IERC4262-withdraw} */ | ||
function withdraw( | ||
uint256 oethAmount, | ||
address receiver, | ||
address owner | ||
) public override returns (uint256 woethAmount) { | ||
if (oethAmount == 0) return 0; | ||
|
||
uint256 creditsBefore = _getOETHCredits(); | ||
woethAmount = super.withdraw(oethAmount, receiver, owner); | ||
oethCreditsHighres -= creditsBefore - _getOETHCredits(); | ||
trackedAssets -= oethAmount; | ||
scheduleYield(); | ||
} | ||
|
||
/** @dev See {IERC4262-redeem} */ | ||
function redeem( | ||
uint256 woethAmount, | ||
address receiver, | ||
address owner | ||
) public override returns (uint256 oethAmount) { | ||
if (woethAmount == 0) return 0; | ||
|
||
uint256 creditsBefore = _getOETHCredits(); | ||
oethAmount = super.redeem(woethAmount, receiver, owner); | ||
oethCreditsHighres -= creditsBefore - _getOETHCredits(); | ||
trackedAssets -= oethAmount; | ||
scheduleYield(); | ||
} | ||
|
||
function _transfer( | ||
address sender, | ||
address recipient, | ||
uint256 amount | ||
) internal override { | ||
super._transfer(sender, recipient, amount); | ||
scheduleYield(); | ||
} | ||
|
||
function _min(uint256 a, uint256 b) internal pure returns (uint256) { | ||
return a < b ? a : b; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OETH -> WOETH