Skip to content

Commit

Permalink
Initial work
Browse files Browse the repository at this point in the history
  • Loading branch information
kanewallmann committed Mar 26, 2024
1 parent dd8e4ed commit 40376f9
Show file tree
Hide file tree
Showing 10 changed files with 594 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
cache/
out/

.env

.idea
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

250 changes: 250 additions & 0 deletions src/WRETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import "../lib/solmate/src/tokens/ERC20.sol";
import "./interface/RocketOvmPriceOracle.sol";

import {Test, console} from "forge-std/Test.sol";

/// NOTE: Due to precision loss caused by the fixed point exchange rate, insignificant amounts of rETH will be trapped in
/// this contract permanently. The value of these tokens will be so low that the gas cost to keep track of them greatly
/// exceeds their worth.

/// @author Kane Wallmann (Rocket Pool)
/// @author ERC20 modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
contract WRETH {
ERC20 immutable public rETH;
RocketOvmPriceOracleInterface immutable public oracle;

uint256 constant public decimals = 18;
string constant public name = "Wrapped Rocket Pool ETH";
string constant public symbol = "wrETH";

uint256 public rate;

// Balances denominated in rETH
uint256 internal supplyInTokens;
mapping(address => uint256) internal tokenBalance;

// Allowances denominated in wrETH
mapping(address => mapping(address => uint256)) public allowance;

// EIP-2612 permit
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;

//
// Events
//

event Rebase(uint256 previousRate, uint256 newRate);
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);

//
// Constructor
//

constructor(ERC20 _rETH, RocketOvmPriceOracleInterface _oracle) {
rETH = _rETH;
oracle = _oracle;
// Record the initial rate
rate = oracle.rate();
// Domain separator
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}

//
// Rebasing functions
//

/// @notice Retrieves the current rETH rate from oracle and rebases balances and supply
function rebase() external {
uint256 newRate = oracle.rate();
// Nothing to do
if (newRate == rate) {
return;
}
// Emit event
emit Rebase(rate, newRate);
// Update the rate
rate = newRate;
}

/// @notice Transfers rETH from the caller and mints wrETH
function mint(uint256 _amountTokens) external {
// Calculate the value denominated in ETH
uint256 amountEth = ethForTokens(_amountTokens);
// Transfer that number of token to this contract
require(rETH.transferFrom(msg.sender, address(this), _amountTokens), "Transfer failed");
// Mint wrETH
supplyInTokens += _amountTokens;
// Cannot overflow because the sum of all user balances can't exceed the max uint256 value
unchecked {
tokenBalance[msg.sender] += _amountTokens;
}
// Emit event
emit Transfer(address(0), msg.sender, amountEth);
}

/// @notice Burns wrETH and returns the appropriate amount of rETH to the caller
function burn(uint256 _amountEth) public {
// Calculate the value denominated in rETH
uint256 amountTokens = tokensForEth(_amountEth);
// Transfer that number of token to this contract
require(rETH.transfer(msg.sender, amountTokens), "Transfer failed");
// Burn wrETH
supplyInTokens -= amountTokens;
// Cannot overflow because the sum of all user balances can't exceed the max uint256 value
unchecked {
tokenBalance[msg.sender] -= amountTokens;
}
// Emit event
emit Transfer(msg.sender, address(0), _amountEth);
}

/// @notice Burns the caller's full balance of wrETH and returns rETH
function burnAll() external {
burn(balanceOf(msg.sender));
}

//
// ERC20 logic
//

/// @notice Allows an owner to permit another account to transfer tokens
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

/// @notice Transfers tokens to another account
function transfer(address to, uint256 amount) public virtual returns (bool) {
uint256 amountTokens = tokensForEth(amount);
tokenBalance[msg.sender] -= amountTokens;
// Cannot overflow because the sum of all user balances can't exceed the max uint256 value
unchecked {
tokenBalance[to] += amountTokens;
}
emit Transfer(msg.sender, to, ethForTokens(amountTokens));
return true;
}

/// @notice Transfers tokens from one account to another
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount;
}
uint256 amountTokens = tokensForEth(amount);
tokenBalance[from] -= amountTokens;
// Cannot overflow because the sum of all user balances can't exceed the max uint256 value
unchecked {
tokenBalance[to] += amountTokens;
}
emit Transfer(from, to, ethForTokens(amountTokens));
return true;
}

//
// EIP-2612 logic
//

/// @notice Sets approval for another account based on EIP-2612 permit signature
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}

/// @notice Returns the EIP-712 domain separator for this token
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}

//
// ERC20 views
//

/// @notice Returns the balance of the given account
function balanceOf(address _owner) public view returns (uint256) {
return ethForTokens(tokenBalance[_owner]);
}

/// @notice Returns the total supply of this token
function totalSupply() external view returns (uint256) {
return ethForTokens(supplyInTokens);
}

//
// Internals
//

/// @dev Calculates the amount of rETH the supplied value of ETH is worth
function tokensForEth(uint256 _eth) internal view returns (uint256) {
return _eth * 1 ether / rate;
}

/// @dev Calculates the amount of ETH the supplied value of rETH is worth
function ethForTokens(uint256 _tokens) internal view returns (uint256) {
return _tokens * rate / 1 ether;
}

/// @dev Computes and returns the EIP-712 domain separator
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
}
6 changes: 6 additions & 0 deletions src/interface/RocketOvmPriceOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

contract RocketOvmPriceOracleInterface {
uint256 public rate;
}
14 changes: 14 additions & 0 deletions src/mock/MockOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../interface/RocketOvmPriceOracle.sol";

contract MockOracle is RocketOvmPriceOracleInterface {
constructor (uint256 _rate) {
rate = _rate;
}

function setRate(uint256 _rate) external {
rate = _rate;
}
}
12 changes: 12 additions & 0 deletions src/mock/MockRETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../../lib/solmate/src/tokens/ERC20.sol";

contract MockRETH is ERC20 {
constructor() ERC20("Rocket Pool ETH", "rETH", 18) {}

function mint(address _to, uint256 _amount) external {
_mint(_to, _amount);
}
}
15 changes: 15 additions & 0 deletions src/mock/MockWRETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../WRETH.sol";

contract MockWRETH is WRETH {
constructor(ERC20 rETH, RocketOvmPriceOracleInterface oracle) WRETH(rETH, oracle) {}

function mockMint(address _to, uint256 _amount) external {
supplyInTokens += _amount;
unchecked {
tokenBalance[_to] += _amount;
}
}
}
24 changes: 0 additions & 24 deletions test/Counter.t.sol

This file was deleted.

Loading

0 comments on commit 40376f9

Please sign in to comment.