Skip to content

Commit b16096e

Browse files
authored
SIP-112: EtherWrapper (#1178)
1 parent 1159c8a commit b16096e

28 files changed

+3831
-38241
lines changed

contracts/BaseDebtCache.sol

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import "./interfaces/IEtherCollateral.sol";
1818
import "./interfaces/IEtherCollateralsUSD.sol";
1919
import "./interfaces/IERC20.sol";
2020
import "./interfaces/ICollateralManager.sol";
21+
import "./interfaces/IEtherWrapper.sol";
2122

2223
// https://docs.synthetix.io/contracts/source/contracts/debtcache
2324
contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
@@ -43,21 +44,23 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
4344
bytes32 private constant CONTRACT_ETHERCOLLATERAL = "EtherCollateral";
4445
bytes32 private constant CONTRACT_ETHERCOLLATERAL_SUSD = "EtherCollateralsUSD";
4546
bytes32 private constant CONTRACT_COLLATERALMANAGER = "CollateralManager";
47+
bytes32 private constant CONTRACT_ETHER_WRAPPER = "EtherWrapper";
4648

4749
constructor(address _owner, address _resolver) public Owned(_owner) MixinSystemSettings(_resolver) {}
4850

4951
/* ========== VIEWS ========== */
5052

5153
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
5254
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
53-
bytes32[] memory newAddresses = new bytes32[](7);
55+
bytes32[] memory newAddresses = new bytes32[](8);
5456
newAddresses[0] = CONTRACT_ISSUER;
5557
newAddresses[1] = CONTRACT_EXCHANGER;
5658
newAddresses[2] = CONTRACT_EXRATES;
5759
newAddresses[3] = CONTRACT_SYSTEMSTATUS;
5860
newAddresses[4] = CONTRACT_ETHERCOLLATERAL;
5961
newAddresses[5] = CONTRACT_ETHERCOLLATERAL_SUSD;
6062
newAddresses[6] = CONTRACT_COLLATERALMANAGER;
63+
newAddresses[7] = CONTRACT_ETHER_WRAPPER;
6164
addresses = combineArrays(existingAddresses, newAddresses);
6265
}
6366

@@ -89,6 +92,10 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
8992
return ICollateralManager(requireAndGetAddress(CONTRACT_COLLATERALMANAGER));
9093
}
9194

95+
function etherWrapper() internal view returns (IEtherWrapper) {
96+
return IEtherWrapper(requireAndGetAddress(CONTRACT_ETHER_WRAPPER));
97+
}
98+
9299
function debtSnapshotStaleTime() external view returns (uint) {
93100
return getDebtSnapshotStaleTime();
94101
}
@@ -203,6 +210,10 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
203210
isInvalid = isInvalid || anyTotalLongRateIsInvalid || anyTotalShortRateIsInvalid;
204211
excludedDebt = excludedDebt.add(longValue).add(shortValue);
205212

213+
// 3. EtherWrapper.
214+
// Subtract sETH and sUSD issued by EtherWrapper.
215+
excludedDebt = excludedDebt.add(etherWrapper().totalIssuedSynths());
216+
206217
return (excludedDebt, isInvalid);
207218
}
208219

contracts/BaseSynthetix.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ contract BaseSynthetix is IERC20, ExternStateToken, MixinResolver, ISynthetix {
8686
return issuer().totalIssuedSynths(currencyKey, false);
8787
}
8888

89+
// TODO: refactor the name of this function. It also incorporates the exclusion of
90+
// issued sETH by the EtherWrapper.
8991
function totalIssuedSynthsExcludeEtherCollateral(bytes32 currencyKey) external view returns (uint) {
9092
return issuer().totalIssuedSynths(currencyKey, true);
9193
}

contracts/EtherWrapper.sol

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
pragma solidity ^0.5.16;
2+
3+
// Inheritance
4+
import "./Owned.sol";
5+
import "./interfaces/IAddressResolver.sol";
6+
import "./interfaces/IEtherWrapper.sol";
7+
import "./interfaces/ISynth.sol";
8+
import "./interfaces/IERC20.sol";
9+
import "./interfaces/IWETH.sol";
10+
11+
// Internal references
12+
import "./Pausable.sol";
13+
import "./interfaces/IIssuer.sol";
14+
import "./interfaces/IExchangeRates.sol";
15+
import "./interfaces/IFeePool.sol";
16+
import "./MixinResolver.sol";
17+
import "./MixinSystemSettings.sol";
18+
19+
// Libraries
20+
import "openzeppelin-solidity-2.3.0/contracts/math/SafeMath.sol";
21+
import "./SafeDecimalMath.sol";
22+
23+
// https://docs.synthetix.io/contracts/source/contracts/etherwrapper
24+
contract EtherWrapper is Owned, Pausable, MixinResolver, MixinSystemSettings, IEtherWrapper {
25+
using SafeMath for uint;
26+
using SafeDecimalMath for uint;
27+
28+
/* ========== CONSTANTS ============== */
29+
30+
/* ========== ENCODED NAMES ========== */
31+
32+
bytes32 internal constant sUSD = "sUSD";
33+
bytes32 internal constant sETH = "sETH";
34+
bytes32 internal constant ETH = "ETH";
35+
bytes32 internal constant SNX = "SNX";
36+
37+
/* ========== ADDRESS RESOLVER CONFIGURATION ========== */
38+
bytes32 private constant CONTRACT_SYNTHSETH = "SynthsETH";
39+
bytes32 private constant CONTRACT_SYNTHSUSD = "SynthsUSD";
40+
bytes32 private constant CONTRACT_ISSUER = "Issuer";
41+
bytes32 private constant CONTRACT_EXRATES = "ExchangeRates";
42+
bytes32 private constant CONTRACT_FEEPOOL = "FeePool";
43+
44+
// ========== STATE VARIABLES ==========
45+
IWETH internal _weth;
46+
47+
uint public sETHIssued = 0;
48+
uint public sUSDIssued = 0;
49+
uint public feesEscrowed = 0;
50+
51+
constructor(
52+
address _owner,
53+
address _resolver,
54+
address payable _WETH
55+
) public Owned(_owner) Pausable() MixinSystemSettings(_resolver) {
56+
_weth = IWETH(_WETH);
57+
}
58+
59+
/* ========== VIEWS ========== */
60+
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
61+
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
62+
bytes32[] memory newAddresses = new bytes32[](5);
63+
newAddresses[0] = CONTRACT_SYNTHSETH;
64+
newAddresses[1] = CONTRACT_SYNTHSUSD;
65+
newAddresses[2] = CONTRACT_EXRATES;
66+
newAddresses[3] = CONTRACT_ISSUER;
67+
newAddresses[4] = CONTRACT_FEEPOOL;
68+
addresses = combineArrays(existingAddresses, newAddresses);
69+
return addresses;
70+
}
71+
72+
/* ========== INTERNAL VIEWS ========== */
73+
function synthsUSD() internal view returns (ISynth) {
74+
return ISynth(requireAndGetAddress(CONTRACT_SYNTHSUSD));
75+
}
76+
77+
function synthsETH() internal view returns (ISynth) {
78+
return ISynth(requireAndGetAddress(CONTRACT_SYNTHSETH));
79+
}
80+
81+
function feePool() internal view returns (IFeePool) {
82+
return IFeePool(requireAndGetAddress(CONTRACT_FEEPOOL));
83+
}
84+
85+
function exchangeRates() internal view returns (IExchangeRates) {
86+
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
87+
}
88+
89+
function issuer() internal view returns (IIssuer) {
90+
return IIssuer(requireAndGetAddress(CONTRACT_ISSUER));
91+
}
92+
93+
/* ========== PUBLIC FUNCTIONS ========== */
94+
95+
// ========== VIEWS ==========
96+
97+
function capacity() public view returns (uint _capacity) {
98+
// capacity = max(maxETH - balance, 0)
99+
uint balance = getReserves();
100+
if (balance >= maxETH()) {
101+
return 0;
102+
}
103+
return maxETH().sub(balance);
104+
}
105+
106+
function getReserves() public view returns (uint) {
107+
return _weth.balanceOf(address(this));
108+
}
109+
110+
function totalIssuedSynths() public view returns (uint) {
111+
// This contract issues two different synths:
112+
// 1. sETH
113+
// 2. sUSD
114+
//
115+
// The sETH is always backed 1:1 with WETH.
116+
// The sUSD fees are backed by sETH that is withheld during minting and burning.
117+
return exchangeRates().effectiveValue(sETH, sETHIssued, sUSD).add(sUSDIssued);
118+
}
119+
120+
function calculateMintFee(uint amount) public view returns (uint) {
121+
return amount.multiplyDecimalRound(mintFeeRate());
122+
}
123+
124+
function calculateBurnFee(uint amount) public view returns (uint) {
125+
return amount.multiplyDecimalRound(burnFeeRate());
126+
}
127+
128+
function maxETH() public view returns (uint256) {
129+
return getEtherWrapperMaxETH();
130+
}
131+
132+
function mintFeeRate() public view returns (uint256) {
133+
return getEtherWrapperMintFeeRate();
134+
}
135+
136+
function burnFeeRate() public view returns (uint256) {
137+
return getEtherWrapperBurnFeeRate();
138+
}
139+
140+
function weth() public view returns (IWETH) {
141+
return _weth;
142+
}
143+
144+
/* ========== MUTATIVE FUNCTIONS ========== */
145+
146+
// Transfers `amountIn` WETH to mint `amountIn - fees` sETH.
147+
// `amountIn` is inclusive of fees, calculable via `calculateMintFee`.
148+
function mint(uint amountIn) external notPaused {
149+
require(amountIn <= _weth.allowance(msg.sender, address(this)), "Allowance not high enough");
150+
require(amountIn <= _weth.balanceOf(msg.sender), "Balance is too low");
151+
152+
uint currentCapacity = capacity();
153+
require(currentCapacity > 0, "Contract has no spare capacity to mint");
154+
155+
if (amountIn < currentCapacity) {
156+
_mint(amountIn);
157+
} else {
158+
_mint(currentCapacity);
159+
}
160+
}
161+
162+
// Burns `amountIn` sETH for `amountIn - fees` WETH.
163+
// `amountIn` is inclusive of fees, calculable via `calculateBurnFee`.
164+
function burn(uint amountIn) external notPaused {
165+
uint reserves = getReserves();
166+
require(reserves > 0, "Contract cannot burn sETH for WETH, WETH balance is zero");
167+
168+
// principal = [amountIn / (1 + burnFeeRate)]
169+
uint principal = amountIn.divideDecimal(SafeDecimalMath.unit().add(burnFeeRate()));
170+
171+
if (principal < reserves) {
172+
_burn(principal);
173+
} else {
174+
_burn(reserves);
175+
}
176+
}
177+
178+
function distributeFees() external {
179+
// Normalize fee to sUSD
180+
require(!exchangeRates().rateIsInvalid(ETH), "Currency rate is invalid");
181+
uint amountSUSD = exchangeRates().effectiveValue(ETH, feesEscrowed, sUSD);
182+
183+
// Burn sETH.
184+
synthsETH().burn(address(this), feesEscrowed);
185+
// Pay down as much sETH debt as we burn. Any other debt is taken on by the stakers.
186+
sETHIssued = sETHIssued < feesEscrowed ? 0 : sETHIssued.sub(feesEscrowed);
187+
188+
// Issue sUSD to the fee pool
189+
issuer().synths(sUSD).issue(feePool().FEE_ADDRESS(), amountSUSD);
190+
sUSDIssued = sUSDIssued.add(amountSUSD);
191+
192+
// Tell the fee pool about this
193+
feePool().recordFeePaid(amountSUSD);
194+
195+
feesEscrowed = 0;
196+
}
197+
198+
// ========== RESTRICTED ==========
199+
200+
/**
201+
* @notice Fallback function
202+
*/
203+
function() external payable {
204+
revert("Fallback disabled, use mint()");
205+
}
206+
207+
/* ========== INTERNAL FUNCTIONS ========== */
208+
209+
function _mint(uint amountIn) internal {
210+
// Calculate minting fee.
211+
uint feeAmountEth = calculateMintFee(amountIn);
212+
uint principal = amountIn.sub(feeAmountEth);
213+
214+
// Transfer WETH from user.
215+
_weth.transferFrom(msg.sender, address(this), amountIn);
216+
217+
// Mint `amountIn - fees` sETH to user.
218+
synthsETH().issue(msg.sender, principal);
219+
220+
// Escrow fee.
221+
synthsETH().issue(address(this), feeAmountEth);
222+
feesEscrowed = feesEscrowed.add(feeAmountEth);
223+
224+
// Add sETH debt.
225+
sETHIssued = sETHIssued.add(amountIn);
226+
227+
emit Minted(msg.sender, principal, feeAmountEth, amountIn);
228+
}
229+
230+
function _burn(uint principal) internal {
231+
// for burn, amount is inclusive of the fee.
232+
uint feeAmountEth = calculateBurnFee(principal);
233+
uint amountIn = principal.add(feeAmountEth);
234+
235+
require(amountIn <= IERC20(address(synthsETH())).allowance(msg.sender, address(this)), "Allowance not high enough");
236+
require(amountIn <= IERC20(address(synthsETH())).balanceOf(msg.sender), "Balance is too low");
237+
238+
// Burn `amountIn` sETH from user.
239+
synthsETH().burn(msg.sender, amountIn);
240+
// sETH debt is repaid by burning.
241+
sETHIssued = sETHIssued < principal ? 0 : sETHIssued.sub(principal);
242+
243+
// We use burn/issue instead of burning the principal and transferring the fee.
244+
// This saves an approval and is cheaper.
245+
// Escrow fee.
246+
synthsETH().issue(address(this), feeAmountEth);
247+
// We don't update sETHIssued, as only the principal was subtracted earlier.
248+
feesEscrowed = feesEscrowed.add(feeAmountEth);
249+
250+
// Transfer `amount - fees` WETH to user.
251+
_weth.transfer(msg.sender, principal);
252+
253+
emit Burned(msg.sender, principal, feeAmountEth, amountIn);
254+
}
255+
256+
/* ========== EVENTS ========== */
257+
event Minted(address indexed account, uint principal, uint fee, uint amountIn);
258+
event Burned(address indexed account, uint principal, uint fee, uint amountIn);
259+
}

contracts/FeePool.sol

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import "./interfaces/IDelegateApprovals.sol";
2626
import "./interfaces/IRewardsDistribution.sol";
2727
import "./interfaces/IEtherCollateralsUSD.sol";
2828
import "./interfaces/ICollateralManager.sol";
29+
import "./interfaces/IEtherWrapper.sol";
2930

3031
// https://docs.synthetix.io/contracts/source/contracts/feepool
3132
contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePool {
@@ -73,6 +74,7 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
7374
bytes32 private constant CONTRACT_ETH_COLLATERAL_SUSD = "EtherCollateralsUSD";
7475
bytes32 private constant CONTRACT_COLLATERALMANAGER = "CollateralManager";
7576
bytes32 private constant CONTRACT_REWARDSDISTRIBUTION = "RewardsDistribution";
77+
bytes32 private constant CONTRACT_ETHER_WRAPPER = "EtherWrapper";
7678

7779
/* ========== ETERNAL STORAGE CONSTANTS ========== */
7880

@@ -91,7 +93,7 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
9193
/* ========== VIEWS ========== */
9294
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
9395
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
94-
bytes32[] memory newAddresses = new bytes32[](12);
96+
bytes32[] memory newAddresses = new bytes32[](13);
9597
newAddresses[0] = CONTRACT_SYSTEMSTATUS;
9698
newAddresses[1] = CONTRACT_SYNTHETIX;
9799
newAddresses[2] = CONTRACT_FEEPOOLSTATE;
@@ -104,6 +106,7 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
104106
newAddresses[9] = CONTRACT_ETH_COLLATERAL_SUSD;
105107
newAddresses[10] = CONTRACT_REWARDSDISTRIBUTION;
106108
newAddresses[11] = CONTRACT_COLLATERALMANAGER;
109+
newAddresses[12] = CONTRACT_ETHER_WRAPPER;
107110
addresses = combineArrays(existingAddresses, newAddresses);
108111
}
109112

@@ -155,6 +158,10 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
155158
return IRewardsDistribution(requireAndGetAddress(CONTRACT_REWARDSDISTRIBUTION));
156159
}
157160

161+
function etherWrapper() internal view returns (IEtherWrapper) {
162+
return IEtherWrapper(requireAndGetAddress(CONTRACT_ETHER_WRAPPER));
163+
}
164+
158165
function issuanceRatio() external view returns (uint) {
159166
return getIssuanceRatio();
160167
}
@@ -247,6 +254,8 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
247254
require(getFeePeriodDuration() > 0, "Fee Period Duration not set");
248255
require(_recentFeePeriodsStorage(0).startTime <= (now - getFeePeriodDuration()), "Too early to close fee period");
249256

257+
etherWrapper().distributeFees();
258+
250259
// Note: when FEE_PERIOD_LENGTH = 2, periodClosing is the current period & periodToRollover is the last open claimable period
251260
FeePeriod storage periodClosing = _recentFeePeriodsStorage(FEE_PERIOD_LENGTH - 2);
252261
FeePeriod storage periodToRollover = _recentFeePeriodsStorage(FEE_PERIOD_LENGTH - 1);
@@ -721,8 +730,12 @@ contract FeePool is Owned, Proxyable, LimitedSetup, MixinSystemSettings, IFeePoo
721730
bool isSynth = issuer().synthsByAddress(msg.sender) != bytes32(0);
722731
bool isEtherCollateralsUSD = msg.sender == address(etherCollateralsUSD());
723732
bool isCollateral = collateralManager().hasCollateral(msg.sender);
733+
bool isEtherWrapper = msg.sender == address(etherWrapper());
724734

725-
require(isExchanger || isSynth || isEtherCollateralsUSD || isCollateral, "Only Internal Contracts");
735+
require(
736+
isExchanger || isSynth || isEtherCollateralsUSD || isCollateral || isEtherWrapper,
737+
"Only Internal Contracts"
738+
);
726739
_;
727740
}
728741

0 commit comments

Comments
 (0)