Skip to content

Commit 1d036fa

Browse files
L2 Arbitrum Deployment (#77)
* first cut of l2 oracle and core contracts * add fuzz tests * update fuzz test runs and max fuzz rejects * l2 deployment * add oracle and oracle pass through to the deployment, updated Config.ts, removed unused deployment files * validate current and previous month, also validate change rate basis points * add chainlink l2 info * override function ownerSyncOraclePrice * house cleaning, remove sync logic * remove _setStartTime * update mint fee to 50bps, complete etherscan verification in l2 deploy script, update comments * update unit tests to check remaining time * add tests for L2Core, add more assertions for L2ScalingPriceOracle, add USDC PSM into deploy script * add more assertions to L2Core test * polish l2 core tests * remove empty newline in deploy script, add usdc psm in arbitrum config * add govern role to config, add additional validation around PCV Guardian and Guard Admin roles in l2 deployment * add additional checks that core contract is correct on deployed contracts * update deploy script * Add integration test for L2 oracle, comment in L2Core changed from Arbitrum to L2 * refactor deployment script to use mainnetAddresses.ts instead of addresses from Config.ts * L2 deploy refactor into respective deploy, configuration, validation, and block explorer verify scripts * add arbitrum smart contracts * validated deployment on arbitrum, adjusted scripts for updates to deploy script * change type to l2 scaling price oracle in types.ts
1 parent 271b580 commit 1d036fa

21 files changed

+1731
-43
lines changed

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ MAINNET_RARI_POOL_8_COMPTROLLER=0xc54172e34046c1653d1920d40333dd358c7a1af4
3535
MAINNET_RARI_POOL_8_FEI=0xd8553552f8868C1Ef160eEdf031cF0BCf9686945
3636
MAINNET_RARI_POOL_8_TRIBE=0xFd3300A9a74b3250F1b2AbC12B47611171910b07
3737
MAINNET_RARI_POOL_8_ETH=0xbB025D470162CC5eA24daF7d4566064EE7f5F111
38-
MAINNET_RARI_POOL_8_DAI=0x7e9cE3CAa9910cc048590801e64174957Ed41d43
38+
MAINNET_RARI_POOL_8_DAI=0x7e9cE3CAa9910cc048590801e64174957Ed41d43
39+
ETHERSCAN_KEY=etherscan_key
40+
ARBISCAN_KEY=arbiscan_key
41+
NODE_OPTIONS=--max-old-space-size=4096
42+
ETH_PRIVATE_KEY=insert_private_key_here

contracts/core/L2Core.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import {Vcon} from "../vcon/Vcon.sol";
5+
import {IVolt, Volt, IERC20} from "../volt/Volt.sol";
6+
import {ICore} from "./ICore.sol";
7+
import {Permissions} from "./Permissions.sol";
8+
9+
/// @title Source of truth for VOLT Protocol on L2
10+
/// @author Volt Protocol
11+
/// @notice maintains roles, access control, Volt, Vcon, and the Vcon treasury
12+
contract L2Core is ICore, Permissions {
13+
/// @notice the address of the VOLT contract
14+
IVolt public immutable override volt;
15+
16+
/// @notice the address of the Vcon contract
17+
IERC20 public override vcon;
18+
19+
constructor(IVolt _volt) {
20+
volt = _volt;
21+
/// give msg.sender the governor role
22+
_setupGovernor(msg.sender);
23+
}
24+
25+
/// @notice governor only function to set the VCON token
26+
function setVcon(IERC20 _vcon) external onlyGovernor {
27+
vcon = _vcon;
28+
29+
emit VconUpdate(_vcon);
30+
}
31+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import {CoreRef} from "./../refs/CoreRef.sol";
5+
import {L2ScalingPriceOracle} from "./../oracle/L2ScalingPriceOracle.sol";
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
8+
/// @notice Testing contract that allows for updates without mocking chainlink calls
9+
contract MockL2ScalingPriceOracle is L2ScalingPriceOracle {
10+
address owner;
11+
12+
constructor(
13+
address _oracle,
14+
bytes32 _jobid,
15+
uint256 _fee,
16+
uint128 _currentMonth,
17+
uint128 _previousMonth,
18+
uint256 _actualStartTime,
19+
uint256 _startingOraclePrice
20+
)
21+
L2ScalingPriceOracle(
22+
_oracle,
23+
_jobid,
24+
_fee,
25+
_currentMonth,
26+
_previousMonth,
27+
_actualStartTime,
28+
_startingOraclePrice
29+
)
30+
{
31+
owner = msg.sender;
32+
}
33+
34+
function fulfill(uint256 _cpiData) external {
35+
_updateCPIData(_cpiData);
36+
}
37+
38+
function compoundInterest() public {
39+
_oracleUpdateChangeRate(monthlyChangeRateBasisPoints);
40+
}
41+
42+
function setStartTime(uint256 newStartTime) public {
43+
startTime = newStartTime;
44+
}
45+
46+
function setStartTimeAndCompoundInterest(uint256 newStartTime) public {
47+
setStartTime(newStartTime);
48+
compoundInterest();
49+
}
50+
51+
function rescueTokens(
52+
IERC20 token,
53+
address to,
54+
uint256 amount
55+
) external {
56+
require(msg.sender == owner, "!owner");
57+
58+
token.transfer(to, amount);
59+
}
60+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import {Decimal} from "../external/Decimal.sol";
5+
import {Constants} from "./../Constants.sol";
6+
import {Deviation} from "./../utils/Deviation.sol";
7+
import {ScalingPriceOracle} from "./ScalingPriceOracle.sol";
8+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
9+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
10+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
11+
12+
/// @notice contract that receives a chainlink price feed and then linearly interpolates that rate over
13+
/// a 28 day period into the VOLT price. Interest is compounded monthly when the rate is updated
14+
/// Specifically built for L2 to allow a deployment that is mid-month
15+
/// @author Elliot Friedman
16+
contract L2ScalingPriceOracle is ScalingPriceOracle {
17+
/// @param _oracle address of chainlink data provider
18+
/// @param _jobid job id
19+
/// @param _fee maximum fee paid to chainlink data provider
20+
/// @param _currentMonth current month's inflation data
21+
/// @param _previousMonth previous month's inflation data
22+
/// @param _actualStartTime unix timestamp of Oracle Price interpolation starting time
23+
/// @param _startingOraclePrice starting oracle price
24+
constructor(
25+
address _oracle,
26+
bytes32 _jobid,
27+
uint256 _fee,
28+
uint128 _currentMonth,
29+
uint128 _previousMonth,
30+
uint256 _actualStartTime,
31+
uint256 _startingOraclePrice
32+
) ScalingPriceOracle(_oracle, _jobid, _fee, _currentMonth, _previousMonth) {
33+
/// ensure start time is not more than 28 days ago
34+
require(
35+
_actualStartTime > block.timestamp - TIMEFRAME,
36+
"L2ScalingPriceOracle: Start time too far in the past"
37+
);
38+
startTime = _actualStartTime;
39+
40+
/// ensure starting oracle price is greater than or equal to 1
41+
require(
42+
_startingOraclePrice >= 1e18,
43+
"L2ScalingPriceOracle: Starting oracle price too low"
44+
);
45+
oraclePrice = _startingOraclePrice;
46+
}
47+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import {Vm} from "./../unit/utils/Vm.sol";
5+
import {DSTest} from "./../unit/utils/DSTest.sol";
6+
import {getCore, getAddresses, FeiTestAddresses} from "./../unit/utils/Fixtures.sol";
7+
import {MockScalingPriceOracle} from "../../mock/MockScalingPriceOracle.sol";
8+
import {MockL2ScalingPriceOracle} from "../../mock/MockL2ScalingPriceOracle.sol";
9+
import {MockChainlinkToken} from "../../mock/MockChainlinkToken.sol";
10+
import {Decimal} from "./../../external/Decimal.sol";
11+
import {ScalingPriceOracle} from "./../../oracle/ScalingPriceOracle.sol";
12+
import {L2ScalingPriceOracle} from "./../../oracle/L2ScalingPriceOracle.sol";
13+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
14+
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
15+
16+
import {console} from "hardhat/console.sol";
17+
18+
contract IntegrationTestL2ScalingPriceOracle is DSTest {
19+
using Decimal for Decimal.D256;
20+
using SafeCast for *;
21+
22+
ScalingPriceOracle private scalingPriceOracle =
23+
ScalingPriceOracle(0x79412660E95F94a4D2d02a050CEA776200939917);
24+
L2ScalingPriceOracle private l2scalingPriceOracle;
25+
26+
/// @notice increase price by x% per month
27+
int256 public monthlyChangeRateBasisPoints =
28+
scalingPriceOracle.monthlyChangeRateBasisPoints();
29+
30+
/// @notice the current month's CPI data from ScalingPriceOracle
31+
uint128 public currentMonth = scalingPriceOracle.currentMonth();
32+
33+
/// @notice the previous month's CPI data from ScalingPriceOracle
34+
uint128 public previousMonth = scalingPriceOracle.previousMonth();
35+
36+
/// @notice address of chainlink oracle to send request
37+
address public oracle = scalingPriceOracle.oracle();
38+
39+
/// @notice job id that retrieves the latest CPI data
40+
bytes32 public jobId = scalingPriceOracle.jobId();
41+
42+
/// @notice fee of 10 link
43+
uint256 public fee = scalingPriceOracle.fee();
44+
45+
/// @notice starting time of the current mainnet scaling price oracle
46+
uint256 public startTime = scalingPriceOracle.startTime();
47+
48+
/// @notice starting price of the current mainnet scaling price oracle
49+
uint256 public startOraclePrice = scalingPriceOracle.oraclePrice();
50+
51+
Vm public constant vm = Vm(HEVM_ADDRESS);
52+
FeiTestAddresses public addresses = getAddresses();
53+
54+
function setUp() public {
55+
l2scalingPriceOracle = new L2ScalingPriceOracle(
56+
oracle,
57+
jobId,
58+
fee,
59+
currentMonth,
60+
previousMonth,
61+
startTime,
62+
startOraclePrice
63+
);
64+
}
65+
66+
function testSetup() public {
67+
assertEq(
68+
scalingPriceOracle.remainingTime(),
69+
l2scalingPriceOracle.remainingTime()
70+
);
71+
assertTrue(
72+
scalingPriceOracle.isTimeEnded() ==
73+
l2scalingPriceOracle.isTimeEnded()
74+
);
75+
assertEq(
76+
scalingPriceOracle.startTime(),
77+
l2scalingPriceOracle.startTime()
78+
);
79+
assertEq(
80+
scalingPriceOracle.oraclePrice(),
81+
l2scalingPriceOracle.oraclePrice()
82+
); /// starting price is correct
83+
assertEq(scalingPriceOracle.oracle(), l2scalingPriceOracle.oracle());
84+
assertEq(scalingPriceOracle.jobId(), l2scalingPriceOracle.jobId());
85+
assertEq(scalingPriceOracle.fee(), l2scalingPriceOracle.fee());
86+
assertEq(
87+
scalingPriceOracle.currentMonth(),
88+
l2scalingPriceOracle.currentMonth()
89+
);
90+
assertEq(
91+
scalingPriceOracle.previousMonth(),
92+
l2scalingPriceOracle.previousMonth()
93+
);
94+
assertEq(
95+
scalingPriceOracle.getMonthlyAPR(),
96+
l2scalingPriceOracle.getMonthlyAPR()
97+
);
98+
assertEq(
99+
scalingPriceOracle.getCurrentOraclePrice(),
100+
l2scalingPriceOracle.getCurrentOraclePrice()
101+
);
102+
}
103+
104+
function _testOraclePriceEquivalence() internal {
105+
assertEq(
106+
scalingPriceOracle.getCurrentOraclePrice(),
107+
l2scalingPriceOracle.getCurrentOraclePrice()
108+
);
109+
assertEq(
110+
scalingPriceOracle.oraclePrice(),
111+
l2scalingPriceOracle.oraclePrice()
112+
);
113+
}
114+
115+
/// positive price action from oracle -- inflation case
116+
function testGetCurrentOraclePriceAfterInterpolation() public {
117+
vm.warp(block.timestamp + 28 days);
118+
_testOraclePriceEquivalence();
119+
}
120+
121+
/// negative price action from oracle -- deflation case
122+
function testPriceDecreaseAfterInterpolation() public {
123+
scalingPriceOracle = new MockScalingPriceOracle(
124+
oracle,
125+
jobId,
126+
fee,
127+
previousMonth, /// flip current and previous months so that rate is -3%
128+
currentMonth
129+
);
130+
l2scalingPriceOracle = new MockL2ScalingPriceOracle(
131+
oracle,
132+
jobId,
133+
fee,
134+
previousMonth, /// flip current and previous months so that rate is -3%
135+
currentMonth,
136+
scalingPriceOracle.startTime(),
137+
1e18
138+
);
139+
140+
vm.warp(block.timestamp + 29 days);
141+
assertEq(
142+
l2scalingPriceOracle.getCurrentOraclePrice(),
143+
scalingPriceOracle.getCurrentOraclePrice()
144+
);
145+
146+
assertEq(
147+
l2scalingPriceOracle.oraclePrice().toInt256() +
148+
(l2scalingPriceOracle.monthlyChangeRateBasisPoints() * 1e18) /
149+
10_000,
150+
l2scalingPriceOracle.getCurrentOraclePrice().toInt256()
151+
);
152+
assertEq(
153+
scalingPriceOracle.oraclePrice().toInt256() +
154+
(scalingPriceOracle.monthlyChangeRateBasisPoints() * 1e18) /
155+
10_000,
156+
scalingPriceOracle.getCurrentOraclePrice().toInt256()
157+
);
158+
assertEq(l2scalingPriceOracle.remainingTime(), 0);
159+
assertEq(scalingPriceOracle.remainingTime(), 0);
160+
161+
_testOraclePriceEquivalence();
162+
}
163+
164+
function testFulfillFailureTimed() public {
165+
if (scalingPriceOracle.isTimeEnded()) {
166+
vm.warp(scalingPriceOracle.startTime()); /// warp to start time if time has ended
167+
}
168+
169+
assertTrue(!scalingPriceOracle.isTimeEnded());
170+
assertTrue(!l2scalingPriceOracle.isTimeEnded());
171+
172+
vm.expectRevert(bytes("Timed: time not ended, init"));
173+
l2scalingPriceOracle.requestCPIData();
174+
175+
vm.expectRevert(bytes("Timed: time not ended, init"));
176+
scalingPriceOracle.requestCPIData();
177+
}
178+
}

contracts/test/unit/core/L2Core.t.sol

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
pragma solidity ^0.8.4;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
import {IVolt} from "../../../volt/Volt.sol";
7+
import {Volt} from "../../../volt/Volt.sol";
8+
import {ICore} from "../../../core/ICore.sol";
9+
import {L2Core, Vcon} from "../../../core/L2Core.sol";
10+
import {Vm} from "./../utils/Vm.sol";
11+
import {DSTest} from "./../utils/DSTest.sol";
12+
import {getL2Core, getAddresses, FeiTestAddresses} from "./../utils/Fixtures.sol";
13+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
14+
import {MockERC20} from "./../../../mock/MockERC20.sol";
15+
16+
contract L2CoreTest is DSTest {
17+
L2Core private core;
18+
19+
Vm public constant vm = Vm(HEVM_ADDRESS);
20+
FeiTestAddresses public addresses = getAddresses();
21+
MockERC20 volt;
22+
Vcon vcon;
23+
24+
function setUp() public {
25+
volt = new MockERC20();
26+
27+
// Deploy Core from Governor address
28+
vm.prank(addresses.governorAddress);
29+
core = new L2Core(IVolt(address(volt)));
30+
vcon = new Vcon(addresses.governorAddress, addresses.governorAddress);
31+
}
32+
33+
function testSetup() public {
34+
assertEq(address(core.volt()), address(volt));
35+
assertEq(address(core.vcon()), address(0)); /// vcon starts set to address 0
36+
37+
assertTrue(core.isGovernor(address(core))); /// core contract is governor
38+
assertTrue(core.isGovernor(addresses.governorAddress)); /// msg.sender of contract is governor
39+
40+
bytes32 governRole = core.GOVERN_ROLE();
41+
/// assert all roles have the proper admin
42+
assertEq(core.getRoleAdmin(core.GOVERN_ROLE()), governRole);
43+
assertEq(core.getRoleAdmin(core.BURNER_ROLE()), governRole);
44+
assertEq(core.getRoleAdmin(core.MINTER_ROLE()), governRole);
45+
assertEq(core.getRoleAdmin(core.GUARDIAN_ROLE()), governRole);
46+
assertEq(core.getRoleAdmin(core.PCV_CONTROLLER_ROLE()), governRole);
47+
48+
/// assert there is only 1 of each role
49+
assertEq(core.getRoleMemberCount(governRole), 2); /// msg.sender of contract and core is governor
50+
assertEq(core.getRoleMemberCount(core.BURNER_ROLE()), 0); /// this role has not been granted
51+
assertEq(core.getRoleMemberCount(core.MINTER_ROLE()), 0); /// this role has not been granted
52+
assertEq(core.getRoleMemberCount(core.GUARDIAN_ROLE()), 0); /// this role has not been granted
53+
assertEq(core.getRoleMemberCount(core.PCV_CONTROLLER_ROLE()), 0); /// this role has not been granted
54+
}
55+
56+
function testGovernorSetsVcon() public {
57+
vm.prank(addresses.governorAddress);
58+
core.setVcon(IERC20(addresses.userAddress));
59+
60+
assertEq(address(core.vcon()), addresses.userAddress);
61+
}
62+
63+
function testNonGovernorFailsSettingVcon() public {
64+
vm.expectRevert("Permissions: Caller is not a governor");
65+
core.setVcon(IERC20(addresses.userAddress));
66+
}
67+
}

0 commit comments

Comments
 (0)