Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Contracts admin.
ADMIN=
# USDC token address. Leave empty to deploy a test one instead.
USDC=
BASETEST_PRIVATE_KEY=
VERIFY=false
27 changes: 16 additions & 11 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,30 @@ jobs:
name: Lint Solidity
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- uses: actions/[email protected]
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js 22
uses: actions/setup-node@v4
with:
node-version: [22.x]

- run: npm ci

node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint:solidity

lint-ts:
name: Lint TS
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- uses: actions/[email protected]
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js 22
uses: actions/setup-node@v4
with:
node-version: [22.x]

- run: npm ci
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint:ts
20 changes: 7 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,19 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.x]
steps:
- name: Checkout code
uses: actions/[email protected]
- uses: actions/[email protected]
uses: actions/checkout@v4
- name: Install Node.js 22
uses: actions/setup-node@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Node.js ${{ matrix.node-version }}
uses: actions/[email protected]
with:
node-version: ${{ matrix.node-version }}
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npm run compile
- name: Hardhat Tests
run: npm run test
- name: Deploy Tests
run: npm run test:deploy
3 changes: 2 additions & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "solhint:recommended",
"rules": {
"max-line-length": ["off", 120],
"func-visibility": ["warn", {"ignoreConstructors": true}]
"func-visibility": ["warn", {"ignoreConstructors": true}],
"func-name-mixedcase": ["off"]
}
}
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# sprinter-liquidity-contracts
# sprinter-liquidity-contracts

Solidity contracts that facilitate Sprinter Liquidity logic

### Install

node 22.x is required
nvm use
npm install
npm run compile

### Test

npm run test

### Deployment

For local development you need to run a local hardhat node and deploy to it:

npm run node
npm run deploy-local

Local deployment wallet private key is: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

To deploy to live networks, create a `.env` file using the `.env.example` and fill in the relevant variables (only the ones needed for your deployment).
You need to have a private key specified.
To deploy to Base Testnet do:

npm run deploy-basetest

Make sure to save the output of the deployment. You can use those later in the `.env` file to run other scripts on the already deployed system.

You could optionally set VERIFY to `true` in order to publish the source code after deployemnt to sourcify.dev.
4 changes: 4 additions & 0 deletions contracts/Deps.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import { TransparentUpgradeableProxy } from '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
155 changes: 155 additions & 0 deletions contracts/LiquidityHub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {
IERC20,
IERC20Metadata,
ERC20Upgradeable,
ERC4626Upgradeable,
Math
} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
import {ERC7201Helper} from './utils/ERC7201Helper.sol';
import {IManagedToken} from './interfaces/IManagedToken.sol';

contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
using Math for uint256;

IManagedToken immutable public SHARES;
bytes32 public constant ASSETS_UPDATE_ROLE = "ASSETS_UPDATE_ROLE";

error ZeroAddress();
error NotImplemented();
error IncompatibleAssetsAndShares();

/// @custom:storage-location erc7201:sprinter.storage.LiquidityHub
struct LiquidityHubStorage {
uint256 totalAssets;
}

bytes32 private constant StorageLocation = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700;

constructor(address shares) {
ERC7201Helper.validateStorageLocation(
StorageLocation,
'sprinter.storage.LiquidityHub'
);
if (shares == address(0)) revert ZeroAddress();
SHARES = IManagedToken(shares);
_disableInitializers();
}

function initialize(IERC20 asset_, address admin) external initializer() {
ERC4626Upgradeable.__ERC4626_init(asset_);
require(
IERC20Metadata(address(asset_)).decimals() <= IERC20Metadata(address(SHARES)).decimals(),
IncompatibleAssetsAndShares()
);
// Deliberately not initializing ERC20Upgradable because its
// functionality is delegated to SHARES.
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}

function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
revert NotImplemented();
}

function symbol() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
revert NotImplemented();
}

function decimals() public pure override returns (uint8) {
revert NotImplemented();
}

function totalSupply() public view virtual override(IERC20, ERC20Upgradeable) returns (uint256) {
return IERC20(address(SHARES)).totalSupply();
}

function balanceOf(address owner) public view virtual override(IERC20, ERC20Upgradeable) returns (uint256) {
return IERC20(address(SHARES)).balanceOf(owner);
}

function transfer(address, uint256) public pure override(IERC20, ERC20Upgradeable) returns (bool) {
revert NotImplemented();
}

function allowance(address, address) public pure override(IERC20, ERC20Upgradeable) returns (uint256) {
// Silences the unreachable code warning from ERC20Upgradeable._spendAllowance().
return 0;
}

function approve(address, uint256) public pure override(IERC20, ERC20Upgradeable) returns (bool) {
revert NotImplemented();
}

function transferFrom(address, address, uint256) public pure override(IERC20, ERC20Upgradeable) returns (bool) {
revert NotImplemented();
}

function totalAssets() public view virtual override returns (uint256) {
return _getStorage().totalAssets;
}

function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
(uint256 supplyShares, uint256 supplyAssets) = _getTotals();
return assets.mulDiv(supplyShares, supplyAssets, rounding);
}

function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
(uint256 supplyShares, uint256 supplyAssets) = _getTotals();
return shares.mulDiv(supplyAssets, supplyShares, rounding);
}

function _getTotals() internal view returns (uint256 supply, uint256 assets) {
supply = totalSupply();
if (supply == 0) {
supply = 10 ** _decimalsOffset();
}
assets = totalAssets();
if (assets == 0) {
assets = 1;
}
return (supply, assets);
}

function _update(address from, address to, uint256 value) internal virtual override {
if (from == address(0)) {
SHARES.mint(to, value);
} else if (to == address(0)) {
SHARES.burn(from, value);
} else {
revert NotImplemented();
}
}

function _spendAllowance(address owner, address spender, uint256 value) internal virtual override {
SHARES.spendAllowance(owner, spender, value);
}

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
super._deposit(caller, receiver, assets, shares);
_getStorage().totalAssets += assets;
}

function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual override {
_getStorage().totalAssets -= assets;
super._withdraw(caller, receiver, owner, assets, shares);
}

function _decimalsOffset() internal view virtual override returns (uint8) {
return IERC20Metadata(address(SHARES)).decimals() - IERC20Metadata(asset()).decimals();
}

function _getStorage() private pure returns (LiquidityHubStorage storage $) {
assembly {
$.slot := StorageLocation
}
}
}
7 changes: 6 additions & 1 deletion contracts/ManagedToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
pragma solidity 0.8.28;

import {ERC20, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {IManagedToken} from './interfaces/IManagedToken.sol';

contract ManagedToken is ERC20Permit {
contract ManagedToken is IManagedToken, ERC20Permit {
address immutable public MANAGER;

error AccessDenied();
Expand All @@ -27,4 +28,8 @@ contract ManagedToken is ERC20Permit {
function burn(address from, uint256 amount) external onlyManager() {
_burn(from, amount);
}

function spendAllowance(address owner, address spender, uint256 value) external onlyManager() {
_spendAllowance(owner, spender, value);
}
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IManagedToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

interface IManagedToken {
function MANAGER() external view returns (address);

function mint(address to, uint256 amount) external;

function burn(address from, uint256 amount) external;

function spendAllowance(address owner, address spender, uint256 value) external;
}
14 changes: 14 additions & 0 deletions contracts/testing/TestUSDC.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestUSDC is ERC20 {
constructor() ERC20("Circle USD", "USDC") {
_mint(msg.sender, 1000 * 10 ** decimals());
}

function decimals() public pure override returns (uint8) {
return 6;
}
}
15 changes: 15 additions & 0 deletions contracts/utils/ERC7201Helper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

library ERC7201Helper {
error InvalidStorageSlot(string namespace);

function validateStorageLocation(bytes32 actual, string memory namespace) internal pure {
bytes32 expected = getStorageLocation(bytes(namespace));
require(actual == expected, InvalidStorageSlot(namespace));
}

function getStorageLocation(bytes memory namespace) internal pure returns(bytes32) {
return keccak256(abi.encode(uint256(keccak256(namespace)) - 1)) & ~bytes32(uint256(0xff));
}
}
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default tseslint.config(
"no-var": "error",
"object-curly-spacing": ["error", "never"],
"prefer-const": "error",
"@typescript-eslint/no-explicit-any": "off",
quotes: ["error", "double"],
semi: "off",
}
Expand Down
8 changes: 8 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const config: HardhatUserConfig = {
},
},
},
networks: {
localhost: {
url: "http://127.0.0.1:8545/",
},
},
sourcify: {
enabled: true,
},
};

export default config;
Loading
Loading