Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ntoken stakefish #361

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
18 changes: 9 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
NETWORK=hardhat #required
ETHERSCAN_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 #required
ETHERSCAN_VERIFICATION=false #required
NETWORK=hardhat
ETHERSCAN_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
ETHERSCAN_VERIFICATION=false
ETHERSCAN_VERIFICATION_JOBS=1
ETHERSCAN_VERIFICATION_MAX_RETRIES=1
BLOCKSCOUT_DISABLE_INDEXER=false
INFURA_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ALCHEMY_KEY=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb #required for forking
DEPLOYER_MNEMONIC=test test test test test test test test test test test junk #required
ALCHEMY_KEY=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
DEPLOYER_MNEMONIC=test test test test test test test test test test test junk
REPORT_GAS=true
MOCHA_JOBS=1
DB_PATH=:memory: #required
DEPLOY_START=0 # set it to <DEPLOY_END> to disable deployment so that you can run tests on exsiting node
DEPLOY_END=21
DB_PATH=:memory:
DEPLOY_START=0
DEPLOY_END=24
# FORK=mainnet
RPC_URL=http://localhost:8545 #required only when we would like to run scripts on existing fork
RPC_URL=http://localhost:8545
DEPLOY_INCREMENTAL=false
6 changes: 3 additions & 3 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn typechain
yarn lint
yarn size
# yarn typechain
# yarn lint
# yarn size
16 changes: 10 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ test-acl-manager:
test-time-lock:
make TEST_TARGET=time_lock_executor.spec.ts test

.PHONY: test-stakefish-nft
test-stakefish-nft:
make TEST_TARGET=_stakefish_nft.spec.ts test

.PHONY: run
run:
npx hardhat run $(SCRIPT_PATH) --network $(NETWORK)
Expand Down Expand Up @@ -380,17 +384,17 @@ deploy-blur-exchange:
deploy-flashClaimRegistry:
make TASK_NAME=deploy:flash-claim-registry run-task

.PHONY: deploy-renounceOwnership
deploy-renounceOwnership:
make TASK_NAME=deploy:renounce-ownership run-task
.PHONY: deploy-p2p-pair-staking
deploy-p2p-pair-staking:
make TASK_NAME=deploy:P2PPairStaking run-task

.PHONY: deploy-timelock
deploy-timelock:
make TASK_NAME=deploy:timelock run-task

.PHONY: deploy-p2p-pair-staking
deploy-p2p-pair-staking:
make TASK_NAME=deploy:P2PPairStaking run-task
.PHONY: deploy-renounceOwnership
deploy-renounceOwnership:
make TASK_NAME=deploy:renounce-ownership run-task

.PHONY: ad-hoc
ad-hoc:
Expand Down
92 changes: 92 additions & 0 deletions contracts/dependencies/openzeppelin/contracts/Base64.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol)

pragma solidity ^0.8.10;

/**
* @dev Provides a set of functions to operate with Base64 strings.
*
* _Available since v4.5._
*/
library Base64 {
/**
* @dev Base64 Encoding/Decoding Table
*/
string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/**
* @dev Converts a `bytes` to its Bytes64 `string` representation.
*/
function encode(bytes memory data) internal pure returns (string memory) {
/**
* Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
* https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
*/
if (data.length == 0) return "";

// Loads the table into memory
string memory table = _TABLE;

// Encoding takes 3 bytes chunks of binary data from `bytes` data parameter
// and split into 4 numbers of 6 bits.
// The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up
// - `data.length + 2` -> Round up
// - `/ 3` -> Number of 3-bytes chunks
// - `4 *` -> 4 characters for each chunk
string memory result = new string(4 * ((data.length + 2) / 3));

/// @solidity memory-safe-assembly
assembly {
// Prepare the lookup table (skip the first "length" byte)
let tablePtr := add(table, 1)

// Prepare result pointer, jump over length
let resultPtr := add(result, 32)

// Run over the input, 3 bytes at a time
for {
let dataPtr := data
let endPtr := add(data, mload(data))
} lt(dataPtr, endPtr) {

} {
// Advance 3 bytes
dataPtr := add(dataPtr, 3)
let input := mload(dataPtr)

// To write each character, shift the 3 bytes (18 bits) chunk
// 4 times in blocks of 6 bits for each character (18, 12, 6, 0)
// and apply logical AND with 0x3F which is the number of
// the previous character in the ASCII table prior to the Base64 Table
// The result is then added to the table to get the character to write,
// and finally write it in the result pointer but with a left shift
// of 256 (1 byte) - 8 (1 ASCII char) = 248 bits

mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
resultPtr := add(resultPtr, 1) // Advance
}

// When data `bytes` is not exactly 3 bytes long
// it is padded with `=` characters at the end
switch mod(mload(data), 3)
case 1 {
mstore8(sub(resultPtr, 1), 0x3d)
mstore8(sub(resultPtr, 2), 0x3d)
}
case 2 {
mstore8(sub(resultPtr, 1), 0x3d)
}
}

return result;
}
}
124 changes: 124 additions & 0 deletions contracts/dependencies/stakefish/StakefishNFTManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

import "../openzeppelin/contracts/ERC721Enumerable.sol";
import "../openzeppelin/contracts/ReentrancyGuard.sol";

import "./interfaces/IStakefishNFTManager.sol";
import "./interfaces/IStakefishValidatorFactory.sol";
import "./interfaces/IStakefishValidator.sol";
import "./interfaces/IStakefishValidatorWallet.sol";

/// @title StakefishNFTManager implementation
/// @notice Extends ERC721, mints and burns NFT representing validators
contract StakefishNFTManager is IStakefishNFTManager, ERC721Enumerable, ReentrancyGuard {
address public immutable override factory;

/// @dev deployed validator contract => tokenId
mapping(address => uint256) private _validatorToToken;

/// @dev tokenId => deployed validator contract
mapping(uint256 => address) private _tokenToValidator;

/// @dev The ID of the next token that will be minted.
uint256 internal _nextId = 1;

modifier isAuthorizedForToken(uint256 tokenId) {
require(_isApprovedOrOwner(msg.sender, tokenId), 'Not approved');
_;
}

constructor(address factory_) ERC721("Stakefish NFT Validator", "SFVLDR") {
require(factory_ != address(0), "missing factory");
factory = factory_;
}

/// PUBLIC WRITE FUNCTIONS
function mint(uint256 validatorCount) external override payable nonReentrant {
require(validatorCount > 0, "wrong value: at least 1 validator must be minted");
require(validatorCount <= IStakefishValidatorFactory(factory).maxValidatorsPerTransaction(), "wrong value: validatorCount exceeds factory limit per transaction");
require(msg.value == validatorCount * 32 ether, "wrong value: must be 32 ETH per validator");
for(uint256 i=0; i < validatorCount; i++) {
_mintOne();
}
}

function verifyAndBurn(address newManager, uint256 tokenId) external override isAuthorizedForToken(tokenId) nonReentrant {
require(newManager != address(this), "new NFTManager cannot be the same as the current NFTManager");
require(IStakefishNFTManager(newManager).validatorOwner( _tokenToValidator[tokenId]) == ownerOf(tokenId), "owner on new NFTManager not confirmed");
address validatorAddress = _tokenToValidator[tokenId];

_burn(tokenId);
_validatorToToken[validatorAddress] = 0;
_tokenToValidator[tokenId] = address(0);

require(IStakefishValidatorWallet(payable(validatorAddress)).getNFTManager() == newManager, "validator not changed to new nft manager");
emit StakefishBurnedWithContract(tokenId, validatorAddress, msg.sender);
}

function multicallStatic(uint256[] calldata tokenIds, bytes[] calldata data) external view override returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
address validatorAddr = this.validatorForTokenId(tokenIds[i]);
require(validatorAddr != address(0), "multicall: address is null");
results[i] = Address.functionStaticCall(validatorAddr, data[i]);
}
return results;
}

function multicall(uint256[] calldata tokenIds, bytes[] calldata data) external override returns (bytes[] memory results) {
results = new bytes[](data.length);
bool isOperator = msg.sender == IStakefishValidatorFactory(factory).operatorAddress();
for (uint256 i = 0; i < data.length; i++) {
address validatorAddr = this.validatorForTokenId(tokenIds[i]);
require(validatorAddr != address(0), "multicall: address is null");
require(ownerOf(tokenIds[i]) == msg.sender || isOperator, "only owner OR operator allowed");
results[i] = Address.functionCall(validatorAddr, data[i]);
}
return results;
}

function claim(address, uint256) external virtual override {
require(false, "migration is unsupported");
}

/// PUBLIC READ FUNCTIONS
function validatorOwner(address validator) external override view returns (address) {
return ownerOf(_validatorToToken[validator]);
}

function validatorForTokenId(uint256 tokenId) external override view returns (address) {
return _tokenToValidator[tokenId];
}

function tokenForValidatorAddr(address validator) external override view returns (uint256) {
return _validatorToToken[validator];
}

function computeAddress(uint256 tokenId) external override view returns (address) {
return IStakefishValidatorFactory(factory).computeAddress(address(this), tokenId);
}

function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory)
{
IStakefishValidator validator = IStakefishValidator(this.validatorForTokenId(tokenId));
return validator.render();
}

/// PRIVATE WRITE FUNCTIONS
function _updateTokenId(uint256 tokenId, address validatorAddr) internal {
require(_validatorToToken[validatorAddr] == 0, "mint: must be empty tokenId");
_validatorToToken[validatorAddr] = tokenId;
_tokenToValidator[tokenId] = validatorAddr;
}

function _mintOne() internal {
uint256 tokenId = _nextId++;
address validatorAddr = IStakefishValidatorFactory(factory).createValidator{value: 32 ether}(tokenId);
_mint(msg.sender, tokenId);
_updateTokenId(tokenId, validatorAddr);
emit StakefishMintedWithContract(tokenId, validatorAddr, msg.sender);
}

}
45 changes: 45 additions & 0 deletions contracts/dependencies/stakefish/StakefishValidatorBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

import "../openzeppelin/contracts/StorageSlot.sol";
import "./interfaces/IStakefishValidatorFactory.sol";
import "./interfaces/IStakefishNFTManager.sol";

/// @title Abstract base contract StakefishValidatorBase
/// @notice Inherited by StakefishValidatorWallet (the proxy) and the implementation contract, both reading from the same slots - which is stored at the proxy level.
abstract contract StakefishValidatorBase {

/// @dev We use slots so that these cannot be overriden by implementation contract
bytes32 internal constant _FACTORY_SLOT = keccak256('stakefish.nftvalidator.factory');
bytes32 internal constant _NFT_MANAGER_SLOT = keccak256('stakefish.nftvalidator.nftmanager');

/// @dev do not declare any state variables (non constant) here. unknown side effects due to proxy/inheritance

modifier isNFTOwner() {
require(getNFTOwner() == msg.sender, "not nft owner");
_;
}

modifier operatorOnly() {
require(IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).operatorAddress() == msg.sender, "not stakefish operator");
_;
}

modifier isNFTMultiCallOrNFTOwner() {
require(getNFTOwner() == msg.sender || isNFTMulticall(), "not nft owner or multicall");
_;
}

function isNFTMulticall() internal view returns (bool) {
return StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value == msg.sender && getNFTOwner() == tx.origin;
}

function getNFTOwner() public view returns (address) {
return IStakefishNFTManager(StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value).validatorOwner(address(this));
}

function getProtocolFee() public view returns (uint256) {
return IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).protocolFee();
}
}
Loading