Skip to content

Commit

Permalink
feat: first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mortimr committed Mar 17, 2022
1 parent 11672fc commit dd8f2c7
Show file tree
Hide file tree
Showing 23 changed files with 5,640 additions and 16 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/Format.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Format Checks

on:
push:
schedule:
- cron: '0 */4 * * *' # every 4 hours, perpetual fuzz testing

jobs:
run-format:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: add node toolchain
uses: actions/[email protected]
with:
node-version: 16.x

- name: cache node_modules
uses: actions/cache@v2
with:
path: |
./node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}

- name: install node dependencies
run: yarn

- name: run formatting checks
run: yarn format:check
31 changes: 31 additions & 0 deletions .github/workflows/Lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Lint Checks

on:
push:
schedule:
- cron: '0 */4 * * *' # every 4 hours, perpetual fuzz testing

jobs:
run-lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: add node toolchain
uses: actions/[email protected]
with:
node-version: 16.x

- name: cache node_modules
uses: actions/cache@v2
with:
path: |
./node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}

- name: install node dependencies
run: yarn

- name: run linting checks
run: yarn lint
18 changes: 18 additions & 0 deletions .github/workflows/Mythril.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Mythril Checks
on:
push:
pull_request:

jobs:
run-mythril:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive

- name: StakingContract
uses: ./actions/mythril
id: staking-contract
with:
contract: './contracts/src/StakingContract'
18 changes: 18 additions & 0 deletions .github/workflows/Tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Tests
on:
push:
pull_request:

jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
with:
version: nightly

- run: forge test --force -vvv
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
cache/
out/
node_modules
3 changes: 3 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 120
}
3 changes: 3 additions & 0 deletions .solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "solhint:default"
}
1 change: 1 addition & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.t.sol
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 🥩
18 changes: 18 additions & 0 deletions actions/mythril/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

# action.yml
name: 'Mythril'
description: 'Run Mythril'
inputs:
contract:
description: 'Contract to test'
required: true
runs:
using: 'docker'
image: 'mythril/myth'
args:
- '-v'
- '4'
- 'analyze'
- ${{ inputs.contract }}
- '--max-depth'
- '15'
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ src = 'src'
out = 'out'
libs = ['lib']
remappings = ['ds-test/=lib/ds-test/src/']
solc = '0.8.11'

# See more config options https://github.com/gakonst/foundry/tree/master/config
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "staking-contracts",
"version": "1.0.0",
"dependencies": {
"prettier": "2.6.0",
"prettier-plugin-solidity": "1.0.0-beta.19",
"solhint": "3.3.7"
},
"scripts": {
"format": "prettier --write ./src",
"format:check": "prettier --check ./src",
"lint": "solhint 'src/**/*.sol'",
"test": "forge"
}
}
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ds-test/=lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
4 changes: 0 additions & 4 deletions src/Contract.sol

This file was deleted.

208 changes: 208 additions & 0 deletions src/StakingContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;

import "./libs/StateLib.sol";
import "./libs/UintLib.sol";
import "./libs/BytesLib.sol";

import "./interfaces/IDepositContract.sol";
import "./test/console.sol";

contract StakingContract {
using StateLib for bytes32;

bytes32 internal constant FEE_SLOT = keccak256("StakingContract.fee");
bytes32 internal constant ADMIN_SLOT = keccak256("StakingContract.admin");
bytes32 internal constant VERSION_SLOT = keccak256("StakingContract.version");
bytes32 internal constant SIGNATURES_SLOT = keccak256("StakingContract.signatures");
bytes32 internal constant PUBLIC_KEYS_SLOT = keccak256("StakingContract.publicKeys");
bytes32 internal constant VALIDATOR_COUNT_SLOT = keccak256("StakingContract.validatorCount");
bytes32 internal constant DEPOSIT_CONTRACT_SLOT = keccak256("StakingContract.depositContract");
bytes32 internal constant PUBLIC_KEY_OWNERSHIP_SLOT = keccak256("StakingContract.publicKeyOwnership");
bytes32 internal constant WITHDRAWAL_CREDENTIALS_SLOT = keccak256("StakingContract.withdrawalCredentials");

uint256 public constant DEPOSIT_SIZE = 32 ether;
uint256 public constant PUBLIC_KEY_LENGTH = 48;
uint256 public constant SIGNATURE_LENGTH = 96;

error Unauthorized();
error AlreadyInitialized();
error InvalidCall();
error InvalidArgument();
error InvalidValue();
error NotEnoughKeys();

event Deposit(address indexed caller, address indexed withdrawer, bytes publicKey, bytes32 publicKeyRoot);

modifier init(uint256 _version) {
if (_version != VERSION_SLOT.getUint256() + 1) {
revert AlreadyInitialized();
}

VERSION_SLOT.setUint256(_version);

_;
}

modifier onlyAdmin() {
if (msg.sender != ADMIN_SLOT.getAddress()) {
revert Unauthorized();
}

_;
}

function initialize_1(
address _admin,
uint256 _fee,
address _depositContract,
bytes32 _withdrawalCredentials
) external init(1) {
ADMIN_SLOT.setAddress(_admin);
FEE_SLOT.setUint256(_fee);
DEPOSIT_CONTRACT_SLOT.setAddress(_depositContract);
WITHDRAWAL_CREDENTIALS_SLOT.setBytes32(_withdrawalCredentials);
}

function getValidatorCount() external view returns (uint256) {
return VALIDATOR_COUNT_SLOT.getUint256();
}

function getKeyCount() external view returns (uint256) {
return PUBLIC_KEYS_SLOT.getStorageBytesArray().value.length;
}

function getWithdrawer(bytes memory _publicKey) external view returns (address) {
bytes32 pubkeyRoot = sha256(BytesLib.pad64(_publicKey));
StateLib.Bytes32ToAddressMappingSlot storage publicKeyOwnership = PUBLIC_KEY_OWNERSHIP_SLOT
.getStorageBytes32ToAddressMapping();
return publicKeyOwnership.value[pubkeyRoot];
}

function getFee() external view returns (uint256) {
return FEE_SLOT.getUint256();
}

function deposit(address _withdrawer) external payable {
_deposit(_withdrawer);
}

receive() external payable {
_deposit(msg.sender);
}

fallback() external payable {
revert InvalidCall();
}

function registerValidatorKeys(bytes[] memory publicKeys, bytes[] memory signatures) external onlyAdmin {
if (publicKeys.length != signatures.length || publicKeys.length == 0) {
revert InvalidArgument();
}

StateLib.BytesArraySlot storage publicKeysStore = PUBLIC_KEYS_SLOT.getStorageBytesArray();
StateLib.BytesArraySlot storage signaturesStore = SIGNATURES_SLOT.getStorageBytesArray();

for (uint256 i; i < publicKeys.length; ) {
if (publicKeys[i].length != 48 || signatures[i].length != 96) {
revert InvalidArgument();
}
publicKeysStore.value.push(publicKeys[i]);
signaturesStore.value.push(signatures[i]);
unchecked {
++i;
}
}
}

function setWithdrawer(bytes memory _publicKey, address _newWithdrawer) external {
bytes32 pubkeyRoot = sha256(BytesLib.pad64(_publicKey));
StateLib.Bytes32ToAddressMappingSlot storage publicKeyOwnership = PUBLIC_KEY_OWNERSHIP_SLOT
.getStorageBytes32ToAddressMapping();

if (msg.sender != publicKeyOwnership.value[pubkeyRoot]) {
revert Unauthorized();
}

publicKeyOwnership.value[pubkeyRoot] = _newWithdrawer;
}

function setFee(uint256 _newFee) external onlyAdmin {
FEE_SLOT.setUint256(_newFee);
}

function _useKeys(
bytes memory _publicKey,
bytes memory _signature,
bytes32 _withdrawalCredentials,
address _withdrawer
) internal {
bytes32 pubkeyRoot = sha256(BytesLib.pad64(_publicKey));
bytes32 signatureRoot = sha256(
abi.encodePacked(
sha256(BytesLib.slice(_signature, 0, 64)),
sha256(BytesLib.pad64(BytesLib.slice(_signature, 64, SIGNATURE_LENGTH - 64)))
)
);

uint256 depositAmount = DEPOSIT_SIZE / 1000000000 wei;
assert(depositAmount * 1000000000 wei == DEPOSIT_SIZE);

bytes32 depositDataRoot = sha256(
abi.encodePacked(
sha256(abi.encodePacked(pubkeyRoot, _withdrawalCredentials)),
sha256(abi.encodePacked(Uint256Lib.toLittleEndian64(depositAmount), signatureRoot))
)
);

uint256 targetBalance = address(this).balance - DEPOSIT_SIZE;

IDepositContract(DEPOSIT_CONTRACT_SLOT.getAddress()).deposit{value: DEPOSIT_SIZE}(
_publicKey,
abi.encodePacked(_withdrawalCredentials),
_signature,
depositDataRoot
);

require(address(this).balance == targetBalance, "EXPECTING_DEPOSIT_TO_HAPPEN");

StateLib.Bytes32ToAddressMappingSlot storage publicKeyOwnership = PUBLIC_KEY_OWNERSHIP_SLOT
.getStorageBytes32ToAddressMapping();

publicKeyOwnership.value[pubkeyRoot] = _withdrawer;

emit Deposit(msg.sender, _withdrawer, _publicKey, pubkeyRoot);
}

function _deposit(address _withdrawer) internal {
uint256 fee = FEE_SLOT.getUint256();

if (msg.value == 0 || msg.value % (DEPOSIT_SIZE + fee) != 0) {
revert InvalidValue();
}

uint256 depositCount = msg.value / (DEPOSIT_SIZE + fee);
uint256 validatorCount = VALIDATOR_COUNT_SLOT.getUint256();
StateLib.BytesArraySlot storage publicKeysStore = PUBLIC_KEYS_SLOT.getStorageBytesArray();
StateLib.BytesArraySlot storage signaturesStore = SIGNATURES_SLOT.getStorageBytesArray();
bytes32 withdrawalCredentials = WITHDRAWAL_CREDENTIALS_SLOT.getBytes32();

if (validatorCount + depositCount > publicKeysStore.value.length) {
revert NotEnoughKeys();
}

for (uint256 i; i < depositCount; ) {
_useKeys(
publicKeysStore.value[validatorCount + i],
signaturesStore.value[validatorCount + i],
withdrawalCredentials,
_withdrawer
);
unchecked {
++i;
}
}

VALIDATOR_COUNT_SLOT.setUint256(validatorCount + depositCount);
}
}
10 changes: 10 additions & 0 deletions src/interfaces/IDepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pragma solidity >=0.8.10;

interface IDepositContract {
function deposit(
bytes calldata pubkey,
bytes calldata withdrawalCredentials,
bytes calldata signature,
bytes32 depositDataRoot
) external payable;
}
Loading

0 comments on commit dd8f2c7

Please sign in to comment.