Skip to content

Commit 73b2b22

Browse files
authored
SIP-127: deploy ERC-1167 proxies for virtual synths (Synthetixio#1191)
1 parent f23240e commit 73b2b22

23 files changed

+405
-38
lines changed

contracts/ExchangerWithVirtualSynth.sol

+25-4
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,44 @@ import "./Exchanger.sol";
55

66
// Internal references
77
import "./interfaces/IVirtualSynth.sol";
8+
import "./MinimalProxyFactory.sol";
89
import "./VirtualSynth.sol";
910

1011
// https://docs.synthetix.io/contracts/source/contracts/exchangerwithvirtualsynth
11-
contract ExchangerWithVirtualSynth is Exchanger {
12-
constructor(address _owner, address _resolver) public Exchanger(_owner, _resolver) {}
12+
contract ExchangerWithVirtualSynth is MinimalProxyFactory, Exchanger {
13+
constructor(address _owner, address _resolver) public MinimalProxyFactory() Exchanger(_owner, _resolver) {}
14+
15+
/* ========== ADDRESS RESOLVER CONFIGURATION ========== */
16+
17+
bytes32 private constant CONTRACT_VIRTUALSYNTH_MASTERCOPY = "VirtualSynthMastercopy";
18+
19+
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
20+
bytes32[] memory existingAddresses = Exchanger.resolverAddressesRequired();
21+
bytes32[] memory newAddresses = new bytes32[](1);
22+
newAddresses[0] = CONTRACT_VIRTUALSYNTH_MASTERCOPY;
23+
addresses = combineArrays(existingAddresses, newAddresses);
24+
}
25+
26+
/* ========== INTERNAL FUNCTIONS ========== */
27+
28+
function _virtualSynthMastercopy() internal view returns (address) {
29+
return requireAndGetAddress(CONTRACT_VIRTUALSYNTH_MASTERCOPY);
30+
}
1331

1432
function _createVirtualSynth(
1533
IERC20 synth,
1634
address recipient,
1735
uint amount,
1836
bytes32 currencyKey
19-
) internal returns (IVirtualSynth vSynth) {
37+
) internal returns (IVirtualSynth) {
2038
// prevent inverse synths from being allowed due to purgeability
2139
require(currencyKey[0] != 0x69, "Cannot virtualize this synth");
2240

23-
vSynth = new VirtualSynth(synth, resolver, recipient, amount, currencyKey);
41+
VirtualSynth vSynth = VirtualSynth(_cloneAsMinimalProxy(_virtualSynthMastercopy(), "Could not create new vSynth"));
42+
vSynth.initialize(synth, resolver, recipient, amount, currencyKey);
2443
emit VirtualSynthCreated(address(synth), recipient, address(vSynth), currencyKey, amount);
44+
45+
return IVirtualSynth(address(vSynth));
2546
}
2647

2748
event VirtualSynthCreated(

contracts/MinimalProxyFactory.sol

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
pragma solidity ^0.5.16;
2+
3+
// https://docs.synthetix.io/contracts/source/contracts/minimalproxyfactory
4+
contract MinimalProxyFactory {
5+
function _cloneAsMinimalProxy(address _base, string memory _revertMsg) internal returns (address clone) {
6+
bytes memory createData = _generateMinimalProxyCreateData(_base);
7+
8+
assembly {
9+
clone := create(
10+
0, // no value
11+
add(createData, 0x20), // data
12+
55 // data is always 55 bytes (10 constructor + 45 code)
13+
)
14+
}
15+
16+
// If CREATE fails for some reason, address(0) is returned
17+
require(clone != address(0), _revertMsg);
18+
}
19+
20+
function _generateMinimalProxyCreateData(address _base) internal pure returns (bytes memory) {
21+
return
22+
abi.encodePacked(
23+
//---- constructor -----
24+
bytes10(0x3d602d80600a3d3981f3),
25+
//---- proxy code -----
26+
bytes10(0x363d3d373d3d3d363d73),
27+
_base,
28+
bytes15(0x5af43d82803e903d91602b57fd5bf3)
29+
);
30+
}
31+
}

contracts/VirtualSynth.sol

+14-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import "./interfaces/IExchanger.sol";
1616
import "openzeppelin-solidity-2.3.0/contracts/token/ERC20/IERC20.sol";
1717

1818
// https://docs.synthetix.io/contracts/source/contracts/virtualsynth
19+
// Note: this contract should be treated as an abstract contract and should not be directly deployed.
20+
// On higher versions of solidity, it would be marked with the `abstract` keyword.
21+
// This contracts implements logic that is only intended to be accessed behind a proxy.
22+
// For the deployed "mastercopy" version, see VirtualSynthMastercopy.
1923
contract VirtualSynth is ERC20, IVirtualSynth {
2024
using SafeMath for uint;
2125
using SafeDecimalMath for uint;
@@ -35,13 +39,18 @@ contract VirtualSynth is ERC20, IVirtualSynth {
3539

3640
bytes32 public currencyKey;
3741

38-
constructor(
42+
bool public initialized = false;
43+
44+
function initialize(
3945
IERC20 _synth,
4046
IAddressResolver _resolver,
4147
address _recipient,
4248
uint _amount,
4349
bytes32 _currencyKey
44-
) public ERC20() {
50+
) external {
51+
require(!initialized, "vSynth already initialized");
52+
initialized = true;
53+
4554
synth = _synth;
4655
resolver = _resolver;
4756
currencyKey = _currencyKey;
@@ -51,6 +60,9 @@ contract VirtualSynth is ERC20, IVirtualSynth {
5160
_mint(_recipient, _amount);
5261

5362
initialSupply = _amount;
63+
64+
// Note: the ERC20 base contract does not have a constructor, so we do not have to worry
65+
// about initializing its state separately
5466
}
5567

5668
// INTERNALS

contracts/VirtualSynthMastercopy.sol

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pragma solidity ^0.5.16;
2+
3+
import "./VirtualSynth.sol";
4+
5+
// https://docs.synthetix.io/contracts/source/contracts/virtualsynthmastercopy
6+
// Note: this is the "frozen" mastercopy of the VirtualSynth contract that should be linked to from
7+
// proxies.
8+
contract VirtualSynthMastercopy is VirtualSynth {
9+
constructor() public ERC20() {
10+
// Freeze mastercopy on deployment so it can never be initialized with real arguments
11+
initialized = true;
12+
}
13+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pragma solidity ^0.5.16;
2+
3+
contract MockPayable {
4+
uint256 public paidTimes;
5+
6+
function pay() external payable {
7+
require(msg.value > 0, "No value paid");
8+
paidTimes = paidTimes + 1;
9+
}
10+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pragma solidity ^0.5.16;
2+
3+
contract MockReverter {
4+
function revertWithMsg(string calldata _msg) external pure {
5+
revert(_msg);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pragma solidity ^0.5.16;
2+
3+
import "../MinimalProxyFactory.sol";
4+
5+
6+
contract TestableMinimalProxyFactory is MinimalProxyFactory {
7+
function cloneAsMinimalProxy(address _base, string calldata _revertMsg) external returns (address clone) {
8+
clone = _cloneAsMinimalProxy(_base, _revertMsg);
9+
emit CloneDeployed(clone, _base);
10+
11+
return clone;
12+
}
13+
14+
function generateMinimalProxyCreateData(address _base) external pure returns (bytes memory) {
15+
return _generateMinimalProxyCreateData(_base);
16+
}
17+
18+
event CloneDeployed(address clone, address base);
19+
}

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const constants = {
9595
AST_FILENAME: 'asts.json',
9696

9797
ZERO_ADDRESS: '0x' + '0'.repeat(40),
98+
ZERO_BYTES32: '0x' + '0'.repeat(64),
9899

99100
OVM_MAX_GAS_LIMIT: '8999999',
100101

publish/deployed/local/config.json

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
"Exchanger": {
6363
"deploy": true
6464
},
65+
"VirtualSynthMastercopy": {
66+
"deploy": true
67+
},
6568
"ExchangeState": {
6669
"deploy": true
6770
},

publish/releases.json

+9
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,14 @@
175175
},
176176
"sources": [],
177177
"sips": [113]
178+
},
179+
{
180+
"name": "Alnilam",
181+
"version": {
182+
"major": 2,
183+
"minor": 42
184+
},
185+
"sources": ["Exchanger", "VirtualSynthMastercopy"],
186+
"sips": [127]
178187
}
179188
]

publish/src/commands/deploy.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -772,12 +772,26 @@ const deploy = async ({
772772
args: [account, addressOf(readProxyForResolver)],
773773
});
774774

775-
const exchanger = await deployer.deployContract({
776-
name: 'Exchanger',
777-
source: useOvm ? 'Exchanger' : 'ExchangerWithVirtualSynth',
778-
deps: ['AddressResolver'],
779-
args: [account, addressOf(readProxyForResolver)],
780-
});
775+
let exchanger;
776+
if (useOvm) {
777+
exchanger = await deployer.deployContract({
778+
name: 'Exchanger',
779+
source: 'Exchanger',
780+
deps: ['AddressResolver'],
781+
args: [account, addressOf(readProxyForResolver)],
782+
});
783+
} else {
784+
exchanger = await deployer.deployContract({
785+
name: 'Exchanger',
786+
source: 'ExchangerWithVirtualSynth',
787+
deps: ['AddressResolver'],
788+
args: [account, addressOf(readProxyForResolver)],
789+
});
790+
791+
await deployer.deployContract({
792+
name: 'VirtualSynthMastercopy',
793+
});
794+
}
781795

782796
const exchangeState = await deployer.deployContract({
783797
name: 'ExchangeState',

test/contracts/Exchanger.spec.js

+6
Original file line numberDiff line numberDiff line change
@@ -3705,6 +3705,8 @@ contract('Exchanger (spec tests)', async accounts => {
37053705

37063706
describe('When using Synthetix', () => {
37073707
before(async () => {
3708+
const VirtualSynthMastercopy = artifacts.require('VirtualSynthMastercopy');
3709+
37083710
({
37093711
Exchanger: exchanger,
37103712
Synthetix: synthetix,
@@ -3742,6 +3744,10 @@ contract('Exchanger (spec tests)', async accounts => {
37423744
'FlexibleStorage',
37433745
'CollateralManager',
37443746
],
3747+
mocks: {
3748+
// Use a real VirtualSynthMastercopy so the spec tests can interrogate deployed vSynths
3749+
VirtualSynthMastercopy: await VirtualSynthMastercopy.new(),
3750+
},
37453751
}));
37463752

37473753
// Send a price update to guarantee we're not stale.

test/contracts/ExchangerWithVirtualSynth.behaviors.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module.exports = function({ accounts }) {
1313
});
1414

1515
beforeEach(async () => {
16+
const VirtualSynthMastercopy = artifacts.require('VirtualSynthMastercopy');
17+
1618
({ mocks: this.mocks, resolver: this.resolver } = await prepareSmocks({
1719
contracts: [
1820
'DebtCache',
@@ -26,6 +28,10 @@ module.exports = function({ accounts }) {
2628
'SystemStatus',
2729
'TradingRewards',
2830
],
31+
mocks: {
32+
// Use a real VirtualSynthMastercopy so the unit tests can interrogate deployed vSynths
33+
VirtualSynthMastercopy: await VirtualSynthMastercopy.new(),
34+
},
2935
accounts: accounts.slice(10), // mock using accounts after the first few
3036
}));
3137
});
@@ -99,7 +105,7 @@ module.exports = function({ accounts }) {
99105
cb();
100106
});
101107
},
102-
whenMockedASynthToIssueAmdBurn: cb => {
108+
whenMockedASynthToIssueAndBurn: cb => {
103109
describe(`when mocked a synth to burn`, () => {
104110
beforeEach(async () => {
105111
// create and share the one synth for all Issuer.synths() calls

test/contracts/ExchangerWithVirtualSynth.unit.js

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
'use strict';
22

3-
const { artifacts, contract } = require('hardhat');
3+
const { artifacts, contract, web3 } = require('hardhat');
44

55
const { assert } = require('./common');
66

7-
const { onlyGivenAddressCanInvoke, ensureOnlyExpectedMutativeFunctions } = require('./helpers');
7+
const {
8+
onlyGivenAddressCanInvoke,
9+
ensureOnlyExpectedMutativeFunctions,
10+
getEventByName,
11+
buildMinimalProxyCode,
12+
} = require('./helpers');
813

914
const { toBytes32 } = require('../..');
1015

@@ -73,7 +78,7 @@ contract('ExchangerWithVirtualSynth (unit tests)', async accounts => {
7378
() => {
7479
behaviors.whenMockedEffectiveRateAsEqual(() => {
7580
behaviors.whenMockedLastNRates(() => {
76-
behaviors.whenMockedASynthToIssueAmdBurn(() => {
81+
behaviors.whenMockedASynthToIssueAndBurn(() => {
7782
behaviors.whenMockedExchangeStatePersistance(() => {
7883
it('it reverts trying to create a virtual synth with no supply', async () => {
7984
await assert.revert(
@@ -123,7 +128,7 @@ contract('ExchangerWithVirtualSynth (unit tests)', async accounts => {
123128
() => {
124129
behaviors.whenMockedEffectiveRateAsEqual(() => {
125130
behaviors.whenMockedLastNRates(() => {
126-
behaviors.whenMockedASynthToIssueAmdBurn(() => {
131+
behaviors.whenMockedASynthToIssueAndBurn(() => {
127132
behaviors.whenMockedExchangeStatePersistance(() => {
128133
describe('when invoked', () => {
129134
let txn;
@@ -147,13 +152,14 @@ contract('ExchangerWithVirtualSynth (unit tests)', async accounts => {
147152
recipient: owner,
148153
});
149154
});
150-
describe('when interrogating the Virtual Synths construction params', () => {
155+
describe('when interrogating the Virtual Synths', () => {
151156
let vSynth;
152157
beforeEach(async () => {
153-
const { vSynth: vSynthAddress } = txn.logs.find(
154-
({ event }) => event === 'VirtualSynthCreated'
155-
).args;
156-
vSynth = await artifacts.require('VirtualSynth').at(vSynthAddress);
158+
const VirtualSynth = artifacts.require('VirtualSynth');
159+
vSynth = await VirtualSynth.at(
160+
getEventByName({ tx: txn, name: 'VirtualSynthCreated' }).args
161+
.vSynth
162+
);
157163
});
158164
it('the vSynth has the correct synth', async () => {
159165
assert.equal(
@@ -175,6 +181,13 @@ contract('ExchangerWithVirtualSynth (unit tests)', async accounts => {
175181
);
176182
assert.equal(this.mocks.synth.smocked.issue.calls[0][1], amount);
177183
});
184+
it('the vSynth is an ERC-1167 minimal proxy instead of a full Virtual Synth', async () => {
185+
const vSynthCode = await web3.eth.getCode(vSynth.address);
186+
assert.equal(
187+
vSynthCode,
188+
buildMinimalProxyCode(this.mocks.VirtualSynthMastercopy.address)
189+
);
190+
});
178191
});
179192
});
180193
});

0 commit comments

Comments
 (0)