Skip to content

Commit

Permalink
feat(protocol,supplementary-contracts): relocate & allow TokenUnlock …
Browse files Browse the repository at this point in the history
…to deploy and own ProverSets (taikoxyz#17251)

Co-authored-by: dantaik <[email protected]>
  • Loading branch information
dantaik and dantaik authored May 20, 2024
1 parent b658890 commit f3d6ca1
Show file tree
Hide file tree
Showing 17 changed files with 528 additions and 384 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
paths:
- "packages/protocol/**"
pull_request:
paths:
- "packages/protocol/**"

jobs:
build-protocol:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/supplementary-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
paths:
- "packages/supplementary-contracts/**"
pull_request:
paths:
- "packages/supplementary-contracts/**"

jobs:
build-supplementary-contracts:
Expand Down
25 changes: 24 additions & 1 deletion packages/protocol/contract_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,5 +505,28 @@
| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/team/proving/ProverSet.sol:ProverSet |
| __gap | uint256[49] | 202 | 0 | 1568 | contracts/team/proving/ProverSet.sol:ProverSet |
| isProver | mapping(address => bool) | 251 | 0 | 32 | contracts/team/proving/ProverSet.sol:ProverSet |
| __gap | uint256[49] | 252 | 0 | 1568 | contracts/team/proving/ProverSet.sol:ProverSet |
| admin | address | 252 | 0 | 20 | contracts/team/proving/ProverSet.sol:ProverSet |
| __gap | uint256[48] | 253 | 0 | 1536 | contracts/team/proving/ProverSet.sol:ProverSet |

## TokenUnlock
| Name | Type | Slot | Offset | Bytes | Contract |
|----------------|--------------------------|------|--------|-------|--------------------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| _initializing | bool | 0 | 1 | 1 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[50] | 1 | 0 | 1600 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| _owner | address | 51 | 0 | 20 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[49] | 52 | 0 | 1568 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| _pendingOwner | address | 101 | 0 | 20 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[49] | 102 | 0 | 1568 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| addressManager | address | 151 | 0 | 20 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[49] | 152 | 0 | 1568 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __reentry | uint8 | 201 | 0 | 1 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __paused | uint8 | 201 | 1 | 1 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[49] | 202 | 0 | 1568 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| amountVested | uint256 | 251 | 0 | 32 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| recipient | address | 252 | 0 | 20 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| tgeTimestamp | uint64 | 252 | 20 | 8 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| isProverSet | mapping(address => bool) | 253 | 0 | 32 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |
| __gap | uint256[47] | 254 | 0 | 1504 | contracts/team/tokenunlock/TokenUnlock.sol:TokenUnlock |

1 change: 1 addition & 0 deletions packages/protocol/contracts/common/LibStrings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ library LibStrings {
bytes32 internal constant B_PROPOSER = bytes32("proposer");
bytes32 internal constant B_PROPOSER_ONE = bytes32("proposer_one");
bytes32 internal constant B_PROVER_ASSIGNMENT = bytes32("PROVER_ASSIGNMENT");
bytes32 internal constant B_PROVER_SET = bytes32("prover_set");
bytes32 internal constant B_QUOTA_MANAGER = bytes32("quota_manager");
bytes32 internal constant B_SGX_WATCHDOG = bytes32("sgx_watchdog");
bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service");
Expand Down
61 changes: 45 additions & 16 deletions packages/protocol/contracts/team/proving/ProverSet.sol
Original file line number Diff line number Diff line change
@@ -1,63 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../common/EssentialContract.sol";
import "../../common/LibStrings.sol";
import "../../L1/ITaikoL1.sol";

interface IHasRecipient {
function recipient() external view returns (address);
}

/// @title ProverSet
/// @notice A contract that holds TKO token and acts as a Taiko prover. This contract will simply
/// relay `proveBlock` calls to TaikoL1 so msg.sender doesn't need to hold any TKO.
/// @custom:security-contact [email protected]
contract ProverSet is EssentialContract, IERC1271 {
bytes4 private constant _EIP1271_MAGICVALUE = 0x1626ba7e;

mapping(address prover => bool isProver) public isProver;
uint256[49] private __gap;
mapping(address prover => bool isProver) public isProver; // slot 1
address public admin; // slot 2

uint256[48] private __gap;

event ProverEnabled(address indexed prover, bool indexed enabled);
event BlockProvenBy(address indexed prover, uint64 indexed blockId);

error INVALID_STATUS();
error PERMISSION_DENIED();

modifier onlyAuthorized() {
if (msg.sender != admin && msg.sender != IHasRecipient(admin).recipient()) {
revert PERMISSION_DENIED();
}
_;
}

modifier onlyProver() {
if (!isProver[msg.sender]) revert PERMISSION_DENIED();
_;
}

/// @notice Initializes the contract.
function init(address _owner, address _addressManager) external initializer {
function init(
address _owner,
address _admin,
address _addressManager
)
external
nonZeroAddr(_admin)
initializer
{
__Essential_init(_owner, _addressManager);

IERC20 tko = IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false));
admin = _admin;

address taikoL1 = resolve(LibStrings.B_TAIKO, false);
tko.approve(taikoL1, type(uint256).max);

address assignmentHook = resolve(LibStrings.B_ASSIGNMENT_HOOK, false);
tko.approve(assignmentHook, type(uint256).max);
IERC20 tko = IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false));
tko.approve(resolve(LibStrings.B_TAIKO, false), type(uint256).max);
tko.approve(resolve(LibStrings.B_ASSIGNMENT_HOOK, false), type(uint256).max);
}

/// @notice Enables or disables a prover.
function enableProver(address _prover, bool _isProver) external onlyOwner {
function enableProver(address _prover, bool _isProver) external onlyAuthorized {
if (isProver[_prover] == _isProver) revert INVALID_STATUS();
isProver[_prover] = _isProver;

emit ProverEnabled(_prover, _isProver);
}

/// @notice Withdraws Taiko tokens back to the owner address.
function withdraw(uint256 _amount) external onlyOwner {
IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)).transfer(owner(), _amount);
/// @notice Withdraws Taiko tokens back to the admin address.
function withdrawToAdmin(uint256 _amount) external onlyAuthorized {
IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)).transfer(admin, _amount);
}

/// @notice Proves or contests a Taiko block.
function proveBlock(uint64 _blockId, bytes calldata _input) external whenNotPaused {
if (!isProver[msg.sender]) revert PERMISSION_DENIED();

function proveBlock(uint64 _blockId, bytes calldata _input) external onlyProver nonReentrant {
emit BlockProvenBy(msg.sender, _blockId);
ITaikoL1(resolve(LibStrings.B_TAIKO, false)).proveBlock(_blockId, _input);
}

/// @notice Delegates token voting right to a delegatee.
/// @param _delegatee The delegatee to receive the voting right.
function delegate(address _delegatee) external onlyAuthorized nonReentrant {
ERC20VotesUpgradeable(resolve(LibStrings.B_TAIKO_TOKEN, false)).delegate(_delegatee);
}

// This function is necessary for this contract to become an assigned prover.
function isValidSignature(
bytes32 _hash,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../../common/EssentialContract.sol";
import "../../common/LibStrings.sol";
import "../../libs/LibMath.sol";
import "../proving/ProverSet.sol";

/// @title TokenUnlocking
/// @title TokenUnlock
/// @notice Manages the linear unlocking of Taiko tokens over a four-year period.
/// Tokens purchased off-chain are deposited into this contract directly from the `msg.sender`
/// address. Token withdrawals are permitted linearly over four years starting from the Token
/// Generation Event (TGE), with no withdrawals allowed during the first year.
/// A separate instance of this contract is deployed for each recipient.
/// @custom:security-contact [email protected]
contract TokenUnlocking is OwnableUpgradeable, ReentrancyGuardUpgradeable {
contract TokenUnlock is EssentialContract {
using SafeERC20 for IERC20;
using LibMath for uint256;

uint256 public constant ONE_YEAR = 365 days;
uint256 public constant FOUR_YEARS = 4 * ONE_YEAR;

uint256 public amountVested;
uint256 public amountWithdrawn;
address public recipient;
address public taikoToken;
uint256 public amountVested; // slot 1
address public recipient; // slot 2
uint64 public tgeTimestamp;

uint256[46] private __gap;
mapping(address proverSet => bool valid) public isProverSet; // slot 3

uint256[47] private __gap;

/// @notice Emitted when token is vested.
/// @param amount The newly vested amount.
Expand All @@ -43,45 +47,46 @@ contract TokenUnlocking is OwnableUpgradeable, ReentrancyGuardUpgradeable {
/// @param newRecipient The new recipient address.
event RecipientChanged(address indexed oldRecipient, address indexed newRecipient);

/// @notice Emitted when a new prover set is created.
/// @param proverSet The new prover set.
event ProverSetCreated(address indexed proverSet);

/// @notice Emitted when TKO are deposited to a prover set.
/// @param proverSet The prover set.
/// @param amount The amount of TKO deposited.
event DepositToProverSet(address indexed proverSet, uint256 amount);

error INVALID_PARAM();
error NOT_WITHDRAWABLE();
error NOT_PROVER_SET();
error PERMISSION_DENIED();

modifier onlyRecipient() {
if (msg.sender != recipient) revert PERMISSION_DENIED();
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @notice Initializes the contract.
/// @param _owner The contract owner address.
/// @param _taikoToken The Taiko token address.
/// @param _addressManager The rollup address manager.
/// @param _recipient Who will be the grantee for this contract.
/// @param _tgeTimestamp The token generation event timestamp.
function init(
address _owner,
address _taikoToken,
address _addressManager,
address _recipient,
uint64 _tgeTimestamp
)
external
nonZeroAddr(_recipient)
nonZeroValue(bytes32(uint256(_tgeTimestamp)))
initializer
{
if (
_owner == _recipient || _owner == address(0) || _recipient == address(0)
|| _taikoToken == address(0) || _tgeTimestamp == 0
) {
revert INVALID_PARAM();
}
if (_owner == _recipient) revert INVALID_PARAM();

_transferOwnership(_owner);
__Essential_init(_owner, _addressManager);

recipient = _recipient;
taikoToken = _taikoToken;
tgeTimestamp = _tgeTimestamp;
}

Expand All @@ -93,27 +98,55 @@ contract TokenUnlocking is OwnableUpgradeable, ReentrancyGuardUpgradeable {
amountVested += _amount;
emit TokenVested(_amount);

IERC20(taikoToken).safeTransferFrom(msg.sender, address(this), _amount);
IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)).safeTransferFrom(
msg.sender, address(this), _amount
);
}

/// @notice Create a new prover set.
function createProverSet() external onlyRecipient returns (address proverSet_) {
bytes memory data = abi.encodeCall(ProverSet.init, (owner(), address(this), addressManager));
proverSet_ = address(new ERC1967Proxy(resolve(LibStrings.B_PROVER_SET, false), data));

isProverSet[proverSet_] = true;
emit ProverSetCreated(proverSet_);
}

function depositToProverSet(
address _proverSet,
uint256 _amount
)
external
nonZeroValue(bytes32(_amount))
onlyRecipient
{
if (!isProverSet[_proverSet]) revert NOT_PROVER_SET();

emit DepositToProverSet(_proverSet, _amount);
IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)).safeTransfer(_proverSet, _amount);
}

/// @notice Withdraws all withdrawable tokens.
/// @param _to The address the token will be sent to.
function withdraw(address _to) external nonReentrant {
uint256 amount = amountWithdrawable();
if (amount == 0) revert NOT_WITHDRAWABLE();

address to = _to == address(0) ? recipient : _to;
if (to != recipient && msg.sender != recipient) {
revert PERMISSION_DENIED();
}
/// @param _amount The amount of tokens to withdraw.
function withdraw(
address _to,
uint256 _amount
)
external
nonZeroAddr(_to)
nonZeroValue(bytes32(_amount))
onlyRecipient
nonReentrant
{
if (_amount > amountWithdrawable()) revert NOT_WITHDRAWABLE();

amountWithdrawn += amount;
emit TokenWithdrawn(to, amount);
emit TokenWithdrawn(_to, _amount);

IERC20(taikoToken).safeTransfer(to, amount);
IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false)).safeTransfer(_to, _amount);
}

function changeRecipient(address _newRecipient) external onlyRecipient nonReentrant {
function changeRecipient(address _newRecipient) external onlyRecipient {
if (_newRecipient == address(0) || _newRecipient == recipient) {
revert INVALID_PARAM();
}
Expand All @@ -125,23 +158,27 @@ contract TokenUnlocking is OwnableUpgradeable, ReentrancyGuardUpgradeable {
/// @notice Delegates token voting right to a delegatee.
/// @param _delegatee The delegatee to receive the voting right.
function delegate(address _delegatee) external onlyRecipient nonReentrant {
ERC20VotesUpgradeable(taikoToken).delegate(_delegatee);
ERC20VotesUpgradeable(resolve(LibStrings.B_TAIKO_TOKEN, false)).delegate(_delegatee);
}

/// @notice Returns the amount of token withdrawable.
/// @return The amount of token withdrawable.
function amountWithdrawable() public view returns (uint256) {
return _getAmountUnlocked() - amountWithdrawn;
IERC20 tko = IERC20(resolve(LibStrings.B_TAIKO_TOKEN, false));
uint256 balance = tko.balanceOf(address(this));
uint256 locked = _getAmountLocked();

return balance.max(locked) - locked;
}

function _getAmountUnlocked() private view returns (uint256) {
function _getAmountLocked() private view returns (uint256) {
uint256 _amountVested = amountVested;
if (_amountVested == 0) return 0;

uint256 _tgeTimestamp = tgeTimestamp;

if (block.timestamp < _tgeTimestamp + ONE_YEAR) return 0;
if (block.timestamp >= _tgeTimestamp + FOUR_YEARS) return _amountVested;
return _amountVested * (block.timestamp - _tgeTimestamp) / FOUR_YEARS;
if (block.timestamp < _tgeTimestamp + ONE_YEAR) return _amountVested;
if (block.timestamp >= _tgeTimestamp + FOUR_YEARS) return 0;
return _amountVested * (_tgeTimestamp + FOUR_YEARS - block.timestamp) / FOUR_YEARS;
}
}
1 change: 1 addition & 0 deletions packages/protocol/deployments/gen-layouts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ contracts=(
"RiscZeroVerifier"
"QuotaManager"
"ProverSet"
"TokenUnlock"
)

# Empty the output file initially
Expand Down
Loading

0 comments on commit f3d6ca1

Please sign in to comment.