-
Notifications
You must be signed in to change notification settings - Fork 83
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: Donation attack prevention #2106
base: master
Are you sure you want to change the base?
Changes from 33 commits
2839dc1
2e6be62
5495d4b
d0eb16f
fc5d874
cfec4e5
8405bfc
e03223c
319467f
1142a74
f911ba6
3dc8523
bad282b
07b51cf
c0333fb
2c965c3
df57ef7
13fd4da
7923ddb
c69583f
6822937
ac855d9
1c9fc64
56d0b7d
057469c
1a6a16f
2223600
b34d811
39e8d4b
3615ef7
466f957
52eb244
f0580bc
0a988d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from world import * | ||
|
||
def expect_approximate(woeth_holder, expected_balance): | ||
balance = woeth.balanceOf(woeth_holder) | ||
diff = abs(expected_balance - balance) | ||
if (diff != 0): | ||
raise Exception("Unexpected balance for account: %s".format(woeth_holder)) | ||
|
||
def confirm_balances_after_upgrade(): | ||
expect_approximate("0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", 1013453939109688661944) | ||
expect_approximate("0xC460B0b6c9b578A4Cb93F99A691e16dB96Ee5833", 575896531839923556165) | ||
expect_approximate("0xdca0a2341ed5438e06b9982243808a76b9add6d0", 319671606657733042618) | ||
expect_approximate("0x8a9d46d28003673cd4fe7a56ecfcfa2be6372e64", 182355401624955452064) | ||
expect_approximate("0xf65ecb5610000100befba41b9f9cf5ca32838078", 97352556026536192865) | ||
expect_approximate("0x0a26e7ab5c554232314a8d459eff0ede72333f08", 91628532171545105831) | ||
|
||
|
||
def manipulate_price(): | ||
OETH_WHALE="0xa4C637e0F704745D182e4D38cAb7E7485321d059" | ||
whl = {'from': OETH_WHALE } | ||
|
||
woeth.convertToAssets(1e18) / 1e18 | ||
oeth.transfer(woeth.address, 10_000 * 1e18, whl) | ||
woeth.convertToAssets(1e18) / 1e18 | ||
|
||
oeth.approve(woeth.address, 1e50, whl) | ||
woeth.deposit(5_000 * 1e18, OETH_WHALE, whl) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,17 +6,29 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 | ||
* | ||
* @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. | ||
*/ | ||
|
||
contract WOETH is ERC4626, Governable, Initializable { | ||
using SafeERC20 for IERC20; | ||
using StableMath for uint256; | ||
uint256 public oethCreditsHighres; | ||
bool private _oethCreditsInitialized; | ||
|
||
constructor( | ||
ERC20 underlying_, | ||
|
@@ -31,6 +43,23 @@ contract WOETH is ERC4626, Governable, Initializable { | |
OETH(address(asset())).rebaseOptIn(); | ||
} | ||
|
||
function initialize2() external 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, , ) = OETH(asset()).creditsBalanceOfHighres( | ||
address(this) | ||
); | ||
} | ||
|
||
function name() public view virtual override returns (string memory) { | ||
return "Wrapped OETH"; | ||
} | ||
|
@@ -49,7 +78,84 @@ contract WOETH is ERC4626, Governable, Initializable { | |
external | ||
onlyGovernor | ||
{ | ||
//@dev TODO: we could implement a feature where if anyone sends OETH direclty to | ||
// the contract, that we can let the governor transfer the excess of the token. | ||
Comment on lines
+80
to
+81
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. nit: perhaps we could just treat any donation as "yield"? |
||
require(asset_ != address(asset()), "Cannot collect OETH"); | ||
IERC20(asset_).safeTransfer(governor(), amount_); | ||
} | ||
|
||
/** | ||
* @dev This function converts requested OETH token amount to its underlying OETH | ||
* credits value that is stored internally in OETH.sol and is required in order to | ||
* be able to rebase. | ||
* | ||
* @param oethAmount Amount of OETH to be converted to OETH credits | ||
* @return amount of OETH credits the OETH amount corresponds to | ||
*/ | ||
function _oethToCredits(uint256 oethAmount) internal returns (uint256) { | ||
(, uint256 creditsPerTokenHighres, ) = OETH(asset()) | ||
.creditsBalanceOfHighres(address(this)); | ||
|
||
/** | ||
* Multiplying OETH amount with the creditsPerTokenHighres is exactly the math that | ||
* is internally being done in OETH: | ||
*/ | ||
// solhint-disable-next-line max-line-length | ||
/** https://github.com/OriginProtocol/origin-dollar/blob/2314cccf2933f5c1f76a6549c1f5c9cc935b6f05/contracts/contracts/token/OUSD.sol#L242-L249 | ||
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. nit: Points to older code (pre-yield-delegation). Would be nice to update it to point to latest commit hash on master |
||
* | ||
* This should make sure that the rounding will always be correct / mimic the rounding | ||
* of OETH. | ||
*/ | ||
return oethAmount.mulTruncate(creditsPerTokenHighres); | ||
} | ||
|
||
/** @dev See {IERC4262-totalAssets} */ | ||
function totalAssets() public view virtual override returns (uint256) { | ||
(, uint256 creditsPerTokenHighres, ) = OETH(asset()) | ||
.creditsBalanceOfHighres(address(this)); | ||
|
||
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. If we used in May or may not be such a great idea though to revert in a view function. 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. Hmm, that would be a good check that would detect possible mathematical irregularities (perhaps due to rounding). Though as you point out it would be weird to revert in a view function. Monitoring sounds like a good place for it though. Have Grafana regularly query both credits values and check that the ones in OETH.sol are greater or equal to the ones in WOETH.sol. 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. |
||
return (oethCreditsHighres).divPrecisely(creditsPerTokenHighres); | ||
} | ||
|
||
/** @dev See {IERC4262-deposit} */ | ||
function deposit(uint256 oethAmount, address receiver) | ||
public | ||
virtual | ||
override | ||
returns (uint256 woethAmount) | ||
{ | ||
woethAmount = super.deposit(oethAmount, receiver); | ||
oethCreditsHighres += _oethToCredits(oethAmount); | ||
} | ||
|
||
/** @dev See {IERC4262-mint} */ | ||
function mint(uint256 woethAmount, address receiver) | ||
public | ||
virtual | ||
override | ||
returns (uint256 oethAmount) | ||
{ | ||
oethAmount = super.mint(woethAmount, receiver); | ||
oethCreditsHighres += _oethToCredits(oethAmount); | ||
} | ||
|
||
/** @dev See {IERC4262-withdraw} */ | ||
function withdraw( | ||
uint256 oethAmount, | ||
address receiver, | ||
address owner | ||
) public virtual override returns (uint256 woethAmount) { | ||
woethAmount = super.withdraw(oethAmount, receiver, owner); | ||
oethCreditsHighres -= _oethToCredits(oethAmount); | ||
} | ||
|
||
/** @dev See {IERC4262-redeem} */ | ||
function redeem( | ||
uint256 woethAmount, | ||
address receiver, | ||
address owner | ||
) public virtual override returns (uint256 oethAmount) { | ||
oethAmount = super.redeem(woethAmount, receiver, owner); | ||
oethCreditsHighres -= _oethToCredits(oethAmount); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); | ||
|
||
module.exports = deploymentWithGovernanceProposal( | ||
{ | ||
deployName: "112_upgrade_woeth", | ||
forceDeploy: false, | ||
//forceSkip: true, | ||
reduceQueueTime: true, | ||
deployerIsProposer: false, | ||
// proposalId: | ||
}, | ||
async ({ deployWithConfirmation, ethers }) => { | ||
const cOETHProxy = await ethers.getContract("OETHProxy"); | ||
const cWOETHProxy = await ethers.getContract("WOETHProxy"); | ||
|
||
const dWOETHImpl = await deployWithConfirmation("WOETH", [ | ||
cOETHProxy.address, | ||
"Wrapped OETH", | ||
"WOETH", | ||
]); | ||
|
||
const cWOETH = await ethers.getContractAt("WOETH", cWOETHProxy.address); | ||
|
||
// Governance Actions | ||
// ---------------- | ||
return { | ||
name: `Upgrade WOETH to a new implementation.`, | ||
actions: [ | ||
// 1. Upgrade WOETH | ||
{ | ||
contract: cWOETHProxy, | ||
signature: "upgradeTo(address)", | ||
args: [dWOETHImpl.address], | ||
}, | ||
// 2. Run the second initializer | ||
{ | ||
contract: cWOETH, | ||
signature: "initialize2()", | ||
args: [], | ||
}, | ||
], | ||
}; | ||
} | ||
); |
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.
WOETHBase
inheritsWOETH
, so might as well add some gaps here nowThere 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.
good point, done