Skip to content
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

Wrapped OUSD #957

Merged
merged 13 commits into from
Apr 11, 2022
111 changes: 111 additions & 0 deletions brownie/scripts/wrapped_ousd_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from world import *
import random

# Designed to be used on a fork test
wrapper = load_contract('wrapped_ousd', '0x20E0c5F61124D184101a0A8d9afaeA69F5dAB907')


NUM_TESTS = 100
MAX_REBASE_INCREASE = 0.1

ALICE = "0x0a4c79ce84202b03e95b7a692e5d728d83c44c76"
ALLEN = "0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9"
TWINS_A = [ALICE, ALLEN]

SARAH = "0x002e08000acbbae2155fab7ac01929564949070d"
SHAWN = "0x9845e1909dca337944a0272f1f9f7249833d2d19"
TWINS_B = [SARAH, SHAWN]

TWINS_C = [SARAH.replace('d','a'), SHAWN.replace('d','a')]

ALL_TWINS = [TWINS_A, TWINS_B, TWINS_C]

# Unlocks
unlock(BINANCE)
funder = load_contract("forcefund", "0x21a755560e746289b60b9db3237a556097de01f8")

unlock(vault_core.address)
funder.fund(vault_core, {'from': BINANCE, 'value': 1e18 })

for account in [ALICE, ALLEN, SARAH, SHAWN, TWINS_C[0], TWINS_C[1]]:
unlock(account)
funder.fund(account, {'from': BINANCE, 'value': 1e18 })
ousd.approve(wrapper, 1e70, {'from': account})


def deposit_some(twins):
wrap_twin, control_twin = twins
amount = int(random.randint(0, 1e18)*random.randint(0, 1e9) + random.randint(0, 1e18))
print("Depositing: %s {" % amount)
ousd.mint(control_twin, amount, {'from': vault_core, 'gas_limit': 10000000, 'allow_revert': True})
ousd.mint(wrap_twin, amount, {'from': vault_core, 'gas_limit': 10000000, 'allow_revert': True})
print(ousd.balanceOf(wrap_twin))
deposit_amount = min(amount, ousd.balanceOf(wrap_twin))
wrapper.deposit(deposit_amount, wrap_twin, {'from': wrap_twin, 'gas_limit': 10000000, 'allow_revert': True})
print("}")



def withdraw_some(twins):
wrap_twin, control_twin = twins
max_withdraw = wrapper.convertToAssets(wrapper.balanceOf(wrap_twin))
amount = int(max_withdraw * random.random())
amount = int(max(5, amount - random.randint(0, 1e18)))
print("Withdrawing: %s {" % amount)
wrapper.withdraw(amount, wrap_twin, wrap_twin, {'from': wrap_twin, 'gas_limit': 10000000, 'allow_revert': True})
print("}")


def withdraw_all(twins):
wrap_twin, control_twin = twins
wrapper.redeem(wrapper.balanceOf(wrap_twin), wrap_twin, wrap_twin,{'from': wrap_twin})


def increase_supply():
new_balance = int(ousd.totalSupply() * (random.random() * MAX_REBASE_INCREASE + 1.0))
new_balance += random.randint(0, 1e18)
print("Rebasing up %s OUSD" % c18(new_balance))
ousd.changeSupply(int(new_balance), {'from': vault_core})

def print_balance_diffs(all_twins):
diffs = []
for twins in all_twins:
control_balance = ousd.balanceOf(twins[1])
wrapper_balance = wrapper.maxWithdraw(twins[0])+ousd.balanceOf(twins[0])
diffs.append(str(wrapper_balance - control_balance))
print("🧑‍🚒"," ".join(diffs))


# Run test

chain.snapshot()

deposit_some(TWINS_C)
deposit_some(TWINS_B)
deposit_some(TWINS_A)

for i in range(0, NUM_TESTS):
print("------ %d ------" % i)
twins = [TWINS_A, TWINS_B][random.randint(0,1)]
if random.randint(0, 1) == 1:
deposit_some(twins)
else:
withdraw_some(twins)
print_balance_diffs(ALL_TWINS)
print_balance_diffs(ALL_TWINS)
increase_supply()
print_balance_diffs(ALL_TWINS)
print_balance_diffs(ALL_TWINS)

# After test

withdraw_all(TWINS_A)
withdraw_all(TWINS_B)
withdraw_all(TWINS_C)

print(ousd.balanceOf(TWINS_A[0]))
print(ousd.balanceOf(TWINS_A[1]))
print(ousd.balanceOf(TWINS_B[0]))
print(ousd.balanceOf(TWINS_B[1]))
print(ousd.balanceOf(TWINS_C[0]))
print(ousd.balanceOf(TWINS_C[1]))
43 changes: 43 additions & 0 deletions contracts/contracts/token/WrappedOusd.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { ERC20 } from "../../lib/solmate/src/tokens/ERC20.sol";
import { ERC4626 } from "../../lib/solmate/src/mixins/ERC4626.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { OUSD } from "./OUSD.sol";
import { Governable } from "../governance/Governable.sol";

contract WrappedOusd is ERC4626, Governable {
using SafeERC20 for IERC20;

constructor(
ERC20 _underlying,
string memory _name,
string memory _symbol
) ERC4626(_underlying, _name, _symbol) Governable() {
OUSD(address(_underlying)).rebaseOptOut(); // It's not treated as a contract yet
DanielVF marked this conversation as resolved.
Show resolved Hide resolved
OUSD(address(_underlying)).rebaseOptIn();
}

/**
* @notice Show the total amount of OUSD held by the wrapper
*/
function totalAssets() public view override returns (uint256) {
return ERC20(asset).balanceOf(address(this));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is possibly manipulatable in a cream-hack esque manner no? would make wOUSD unsuitable for borrowing in lending markets

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we're saved here by the fact that the protocol will never buy OUSD for more than $1, or sell it for less than $1. Otherwise, you're right, you'd be able to temporarily bend the price of OUSD, buy up a bunch at $.90 then swap it for wOUSD before redeeming it at full price.

More generally, the best practice is to always use a dedicated oracle for the borrowed asset instead of a derived one to prevent shenanigans with bendy bonding curves.

Copy link
Collaborator Author

@DanielVF DanielVF Mar 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that you can flat out give OUSD to wOUSD, making each wOUSD worth more. Giving away money like this is a loss for the attacker - wOUSD holders, and OUSD holders both are happy and good here. The problem is if a lending platform is using wOUSD as collateral, has lended it back to the attacker and suddenly thinks the attacker can borrow a bajillion dollars.

See these two threads:

This isn't a vulnerability in wOUSD, just something a lending platform has to take into account because the price of WOUSD going up could take place inside the same transaction.

}

/**
* @notice Transfer token to governor. Intended for recovering tokens stuck in
* contract, i.e. mistaken sends. Cannot transfer OUSD
* @param _asset Address for the asset
* @param _amount Amount of the asset to transfer
*/
function transferToken(address _asset, uint256 _amount)
external
onlyGovernor
{
require(_asset != address(asset), "Cannot collect OUSD");
IERC20(_asset).safeTransfer(governor(), _amount);
}
}
16 changes: 16 additions & 0 deletions contracts/deploy/001_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,21 @@ const deployVaultVaultChecker = async () => {
await deployWithConfirmation("VaultValueChecker", [vault.address]);
};

const deployWOusd = async () => {
const { deployerAddr, governorAddr } = await getNamedAccounts();
const sDeployer = await ethers.provider.getSigner(deployerAddr);
const sGovernor = await ethers.provider.getSigner(governorAddr);
const ousd = await ethers.getContract("OUSDProxy");
await deployWithConfirmation("WrappedOusd", [
ousd.address,
"Wrapped OUSD",
"WOUSD",
]);
const wousd = await ethers.getContract("WrappedOusd");
await wousd.connect(sDeployer).transferGovernance(governorAddr);
await wousd.connect(sGovernor).claimGovernance();
};

const main = async () => {
console.log("Running 001_core deployment...");
await deployOracles();
Expand All @@ -722,6 +737,7 @@ const main = async () => {
await deployBuyback();
await deployUniswapV3Pool();
await deployVaultVaultChecker();
await deployWOusd();
console.log("001_core deploy done.");
return true;
};
Expand Down
47 changes: 47 additions & 0 deletions contracts/deploy/039_wrapped_ousd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { deploymentWithProposal, withConfirmation } = require("../utils/deploy");

// Deploy new staking implimentation contract with fix
DanielVF marked this conversation as resolved.
Show resolved Hide resolved
// Upgrade to using it

module.exports = deploymentWithProposal(
{ deployName: "039_wrapped_ousd", forceDeploy: false },
async ({ deployWithConfirmation, getTxOpts, ethers }) => {
const { deployerAddr, governorAddr } = await getNamedAccounts();
const sDeployer = await ethers.provider.getSigner(deployerAddr);

// Current contracts
const cOUSDProxy = await ethers.getContract("OUSDProxy");

// Deployer Actions
// ----------------

// 1. Deploy the new implementation.
const dWrappedOusd = await deployWithConfirmation("WrappedOusd", [
cOUSDProxy.address,
"Wrapped OUSD",
"WOUSD",
]);
const cWousd = await ethers.getContract("WrappedOusd");

// 2. Assign ownership
await withConfirmation(
cWousd
.connect(sDeployer)
.transferGovernance(governorAddr, await getTxOpts())
);

// Governance Actions
// ----------------

return {
name: "Claim WOUSD Governance",
actions: [
// 1. Claim governance
{
contract: cWousd,
signature: "claimGovernance()",
},
],
};
}
);
3 changes: 3 additions & 0 deletions contracts/lib/solmate/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.sol linguist-language=Solidity
.dapprc linguist-language=Shell
.gas-snapshot linguist-language=Julia
3 changes: 3 additions & 0 deletions contracts/lib/solmate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/cache
/node_modules
/out
14 changes: 14 additions & 0 deletions contracts/lib/solmate/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"tabWidth": 2,
"printWidth": 100,

"overrides": [
{
"files": "*.sol",
"options": {
"tabWidth": 4,
"printWidth": 120
}
}
]
}
64 changes: 64 additions & 0 deletions contracts/lib/solmate/src/auth/Auth.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;

/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol)
/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol)
abstract contract Auth {
event OwnerUpdated(address indexed user, address indexed newOwner);

event AuthorityUpdated(address indexed user, Authority indexed newAuthority);

address public owner;

Authority public authority;

constructor(address _owner, Authority _authority) {
owner = _owner;
authority = _authority;

emit OwnerUpdated(msg.sender, _owner);
emit AuthorityUpdated(msg.sender, _authority);
}

modifier requiresAuth() {
require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED");

_;
}

function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) {
Authority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas.

// Checking if the caller is the owner only after calling the authority saves gas in most cases, but be
// aware that this makes protected functions uncallable even to the owner if the authority is out of order.
return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner;
}

function setAuthority(Authority newAuthority) public virtual {
// We check if the caller is the owner first because we want to ensure they can
// always swap out the authority even if it's reverting or using up a lot of gas.
require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig));

authority = newAuthority;

emit AuthorityUpdated(msg.sender, newAuthority);
}

function setOwner(address newOwner) public virtual requiresAuth {
owner = newOwner;

emit OwnerUpdated(msg.sender, newOwner);
}
}

/// @notice A generic interface for a contract which provides authorization data to an Auth instance.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol)
/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol)
interface Authority {
function canCall(
address user,
address target,
bytes4 functionSig
) external view returns (bool);
}
Loading