Skip to content

Commit

Permalink
Add a UUPS proxy scheme (#9)
Browse files Browse the repository at this point in the history
* Add a UUPS proxy scheme

* Remove out-of-date comment from README
  • Loading branch information
oldchili authored Jun 11, 2024
1 parent 31a4159 commit 45c9e12
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 158 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/certora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Install Certora
run: pip3 install certora-cli-beta

- name: Verify ${{ matrix.nst }}
run: make certora-${{ matrix.nst }}
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}
# - name: Verify ${{ matrix.nst }}
# run: make certora-${{ matrix.nst }}
# env:
# CERTORAKEY: ${{ secrets.CERTORAKEY }}
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "lib/token-tests"]
path = lib/token-tests
url = https://github.com/makerdao/token-tests
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/openzeppelin-foundry-upgrades"]
path = lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ This repository includes 3 smart contracts:
### NST token

This is a standard erc20 implementation with regular `permit` functionality + EIP-1271 smart contract signature validation.
There should be only one `wards(address)` set, and that needs to be the `NstJoin` implementation.

The token uses the ERC-1822 UUPS pattern for upgradeability and the ERC-1967 proxy storage slots standard.
It is important that the `NstDeploy` library sequence be used for deploying the token.

#### OZ upgradeability validations

The OZ validations can be run alongside the existing tests:
`VALIDATE=true forge test --ffi --build-info --extra-output storageLayout`

### NstJoin

Expand Down
6 changes: 4 additions & 2 deletions deploy/NstDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
pragma solidity ^0.8.21;

import { ScriptTools } from "dss-test/ScriptTools.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { Nst } from "src/Nst.sol";
import { NstJoin } from "src/NstJoin.sol";
Expand All @@ -34,15 +35,16 @@ library NstDeploy {
address owner,
address daiJoin
) internal returns (NstInstance memory instance) {
address _nst = address(new Nst());
address _nstImp = address(new Nst());
address _nst = address((new ERC1967Proxy(_nstImp, abi.encodeCall(Nst.initialize, ()))));
ScriptTools.switchOwner(_nst, deployer, owner);

address _nstJoin = address(new NstJoin(DaiJoinLike(daiJoin).vat(), _nst));
address _daiNst = address(new DaiNst(daiJoin, _nstJoin));

instance.nst = _nst;
instance.nstImp = _nstImp;
instance.nstJoin = _nstJoin;
instance.daiNst = _daiNst;
instance.owner = owner;
}
}
9 changes: 7 additions & 2 deletions deploy/NstInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { NstInstance } from "./NstInstance.sol";

interface NstLike {
function rely(address) external;
function deny(address) external;
function version() external view returns (string memory);
function getImplementation() external view returns (address);
}

interface NstJoinLike {
Expand All @@ -35,10 +36,14 @@ interface DaiNstLike {
}

library NstInit {

function init(
DssInstance memory dss,
NstInstance memory instance
) internal {
require(keccak256(abi.encodePacked(NstLike(instance.nst).version())) == keccak256(abi.encodePacked("1")), "NstInit/version-does-not-match");
require(NstLike(instance.nst).getImplementation() == instance.nstImp, "NstInit/imp-does-not-match");

require(NstJoinLike(instance.nstJoin).vat() == address(dss.vat), "NstInit/vat-does-not-match");
require(NstJoinLike(instance.nstJoin).nst() == instance.nst, "NstInit/nst-does-not-match");

Expand All @@ -47,9 +52,9 @@ library NstInit {
require(DaiNstLike(instance.daiNst).nstJoin() == instance.nstJoin, "NstInit/nstJoin-does-not-match");

NstLike(instance.nst).rely(instance.nstJoin);
NstLike(instance.nst).deny(instance.owner);

dss.chainlog.setAddress("NST", instance.nst);
dss.chainlog.setAddress("NST_IMP", instance.nstImp);
dss.chainlog.setAddress("NST_JOIN", instance.nstJoin);
dss.chainlog.setAddress("DAI_NST", instance.daiNst);
}
Expand Down
2 changes: 1 addition & 1 deletion deploy/NstInstance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pragma solidity >=0.8.0;

struct NstInstance {
address nst;
address nstImp;
address nstJoin;
address daiNst;
address owner;
}
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts-upgradeable
1 change: 1 addition & 0 deletions lib/openzeppelin-foundry-upgrades
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
forge-std/=lib/openzeppelin-foundry-upgrades/lib/forge-std/src/
23 changes: 16 additions & 7 deletions src/Nst.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@

pragma solidity ^0.8.21;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

interface IERC1271 {
function isValidSignature(
bytes32,
bytes memory
) external view returns (bytes4);
}

contract Nst {
contract Nst is UUPSUpgradeable {
mapping (address => uint256) public wards;

// --- ERC20 Data ---
Expand All @@ -48,8 +50,6 @@ contract Nst {
event Transfer(address indexed from, address indexed to, uint256 value);

// --- EIP712 niceties ---
uint256 public immutable deploymentChainId;
bytes32 private immutable _DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

modifier auth {
Expand All @@ -58,11 +58,20 @@ contract Nst {
}

constructor() {
_disableInitializers(); // Avoid initializing in the context of the implementation
}

// --- Upgradability ---

function initialize() initializer external {
wards[msg.sender] = 1;
emit Rely(msg.sender);
}

function _authorizeUpgrade(address newImplementation) internal override auth {}

deploymentChainId = block.chainid;
_DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid);
function getImplementation() external view returns (address) {
return ERC1967Utils.getImplementation();
}

function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {
Expand All @@ -78,7 +87,7 @@ contract Nst {
}

function DOMAIN_SEPARATOR() external view returns (bytes32) {
return block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid);
return _calculateDomainSeparator(block.chainid);
}

// --- Administration ---
Expand Down Expand Up @@ -222,7 +231,7 @@ contract Nst {
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid),
_calculateDomainSeparator(block.chainid),
keccak256(abi.encode(
PERMIT_TYPEHASH,
owner,
Expand Down
117 changes: 45 additions & 72 deletions test/DaiNst.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,121 +3,94 @@
pragma solidity ^0.8.21;

import "dss-test/DssTest.sol";
import "dss-interfaces/Interfaces.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { Nst } from "src/Nst.sol";
import { NstJoin } from "src/NstJoin.sol";
import { DaiNst } from "src/DaiNst.sol";

contract VatMock {
mapping (address => mapping (address => uint256)) public can;
mapping (address => uint256) public dai;

function either(bool x, bool y) internal pure returns (bool z) {
assembly{ z := or(x, y)}
}

function wish(address bit, address usr) internal view returns (bool) {
return either(bit == usr, can[bit][usr] == 1);
}

function hope(address usr) external {
can[msg.sender][usr] = 1;
}

function suck(address addr, uint256 rad) external {
dai[addr] = dai[addr] + rad;
}

function move(address src, address dst, uint256 rad) external {
require(wish(src, msg.sender), "VatMock/not-allowed");
dai[src] = dai[src] - rad;
dai[dst] = dai[dst] + rad;
}
}

contract Dai is Nst {}

contract DaiJoin is NstJoin {
constructor(address vat_, address dai_) NstJoin(vat_, dai_) {}
}

contract DaiNstTest is DssTest {
VatMock vat;
Dai dai;
DaiJoin daiJoin;
Nst nst;
NstJoin nstJoin;
DaiNst daiNst;
ChainlogAbstract constant chainLog = ChainlogAbstract(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);

VatAbstract vat;
DaiAbstract dai;
DaiJoinAbstract daiJoin;
Nst nst;
NstJoin nstJoin;
DaiNst daiNst;

event DaiToNst(address indexed caller, address indexed usr, uint256 wad);
event NstToDai(address indexed caller, address indexed usr, uint256 wad);

function setUp() public {
vat = new VatMock();
dai = new Dai();
daiJoin = new DaiJoin(address(vat), address(dai));
dai.rely(address(daiJoin));
dai.deny(address(this));
nst = new Nst();
vm.createSelectFork(vm.envString("ETH_RPC_URL"));

vat = VatAbstract(chainLog.getAddress("MCD_VAT"));
dai = DaiAbstract(chainLog.getAddress("MCD_DAI"));
daiJoin = DaiJoinAbstract(chainLog.getAddress("MCD_JOIN_DAI"));
address pauseProxy = chainLog.getAddress("MCD_PAUSE_PROXY");
nst = Nst(address(new ERC1967Proxy(address(new Nst()), abi.encodeCall(Nst.initialize, ()))));
nstJoin = new NstJoin(address(vat), address(nst));
nst.rely(address(nstJoin));
nst.deny(address(this));

daiNst = new DaiNst(address(daiJoin), address(nstJoin));

vat.suck(address(this), 10_000 * RAD);
vm.prank(pauseProxy); vat.suck(address(this), address(this), 10_000 * RAD);
}

function testExchange() public {
uint256 daiSup = dai.totalSupply();
vat.hope(address(daiJoin));
daiJoin.exit(address(this), 10_000 * WAD);
assertEq(dai.balanceOf(address(this)), 10_000 * WAD);
assertEq(dai.totalSupply(), 10_000 * WAD);
assertEq(nst.balanceOf(address(this)), 0);
assertEq(nst.totalSupply(), 0);
assertEq(dai.totalSupply() - daiSup, 10_000 * WAD);
assertEq(nst.balanceOf(address(this)), 0);
assertEq(nst.totalSupply(), 0);

dai.approve(address(daiNst), 4_000 * WAD);
vm.expectEmit(true, true, true, true);
emit DaiToNst(address(this), address(this), 4_000 * WAD);
daiNst.daiToNst(address(this), 4_000 * WAD);
assertEq(dai.balanceOf(address(this)), 6_000 * WAD);
assertEq(dai.totalSupply(), 6_000 * WAD);
assertEq(nst.balanceOf(address(this)), 4_000 * WAD);
assertEq(nst.totalSupply(), 4_000 * WAD);
assertEq(dai.balanceOf(address(this)), 6_000 * WAD);
assertEq(dai.totalSupply() - daiSup, 6_000 * WAD);
assertEq(nst.balanceOf(address(this)), 4_000 * WAD);
assertEq(nst.totalSupply(), 4_000 * WAD);

nst.approve(address(daiNst), 2_000 * WAD);
vm.expectEmit(true, true, true, true);
emit NstToDai(address(this), address(this), 2_000 * WAD);
daiNst.nstToDai(address(this), 2_000 * WAD);
assertEq(dai.balanceOf(address(this)), 8_000 * WAD);
assertEq(dai.totalSupply(), 8_000 * WAD);
assertEq(nst.balanceOf(address(this)), 2_000 * WAD);
assertEq(nst.totalSupply(), 2_000 * WAD);
assertEq(dai.balanceOf(address(this)), 8_000 * WAD);
assertEq(dai.totalSupply() - daiSup, 8_000 * WAD);
assertEq(nst.balanceOf(address(this)), 2_000 * WAD);
assertEq(nst.totalSupply(), 2_000 * WAD);

address receiver = address(123);
assertEq(dai.balanceOf(receiver), 0);
assertEq(nst.balanceOf(receiver), 0);
assertEq(dai.balanceOf(receiver), 0);
assertEq(nst.balanceOf(receiver), 0);

dai.approve(address(daiNst), 1_500 * WAD);
vm.expectEmit(true, true, true, true);
emit DaiToNst(address(this), receiver, 1_500 * WAD);
emit DaiToNst(address(this), receiver, 1_500 * WAD);
daiNst.daiToNst(receiver, 1_500 * WAD);
assertEq(dai.balanceOf(address(this)), 6_500 * WAD);
assertEq(dai.balanceOf(address(this)), 6_500 * WAD);
assertEq(dai.balanceOf(receiver), 0);
assertEq(dai.totalSupply(), 6_500 * WAD);
assertEq(nst.balanceOf(address(this)), 2_000 * WAD);
assertEq(nst.balanceOf(receiver), 1_500 * WAD);
assertEq(nst.totalSupply(), 3_500 * WAD);
assertEq(dai.totalSupply() - daiSup, 6_500 * WAD);
assertEq(nst.balanceOf(address(this)), 2_000 * WAD);
assertEq(nst.balanceOf(receiver), 1_500 * WAD);
assertEq(nst.totalSupply(), 3_500 * WAD);

nst.approve(address(daiNst), 500 * WAD);
vm.expectEmit(true, true, true, true);
emit NstToDai(address(this), receiver, 500 * WAD);
emit NstToDai(address(this), receiver, 500 * WAD);
daiNst.nstToDai(receiver, 500 * WAD);
assertEq(dai.balanceOf(address(this)), 6_500 * WAD);
assertEq(dai.balanceOf(receiver), 500 * WAD);
assertEq(dai.totalSupply(), 7_000 * WAD);
assertEq(nst.balanceOf(address(this)), 1_500 * WAD);
assertEq(nst.balanceOf(receiver), 1_500 * WAD);
assertEq(nst.totalSupply(), 3_000 * WAD);
assertEq(dai.balanceOf(address(this)), 6_500 * WAD);
assertEq(dai.balanceOf(receiver), 500 * WAD);
assertEq(dai.totalSupply() - daiSup, 7_000 * WAD);
assertEq(nst.balanceOf(address(this)), 1_500 * WAD);
assertEq(nst.balanceOf(receiver), 1_500 * WAD);
assertEq(nst.totalSupply(), 3_000 * WAD);
}
}
Loading

0 comments on commit 45c9e12

Please sign in to comment.