Skip to content

feat(protocol): add a few governance/treasury related contracts #19229

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

Merged
merged 33 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
08171a9
Create TokenLocker.sol
dantaik Apr 7, 2025
038319f
add tests
dantaik Apr 7, 2025
2e2f814
add contracts for treasury management
dantaik Apr 7, 2025
c7d02b4
Merge branch 'main' into treasury_contracts
dantaik Apr 7, 2025
dc08b85
Update TaikoTreasuryVault.t.sol
dantaik Apr 7, 2025
85707b3
fmt
dantaik Apr 7, 2025
9387b8f
Update DeployProtocolOnL1.s.sol
dantaik Apr 7, 2025
70f8ea3
Update DeployPacayaL1.s.sol
dantaik Apr 7, 2025
964c946
Update UpgradeDevnetPacayaL1.s.sol
dantaik Apr 7, 2025
70114ed
Update UpgradeDevnetPacayaL2.s.sol
dantaik Apr 7, 2025
8882a6e
Update DeployProtocolOnL1.s.sol
dantaik Apr 7, 2025
93f044b
Merge branch 'fix_deployment_code' into treasury_contracts
dantaik Apr 7, 2025
74f9067
forge fmt & update contract layout tables
dantaik Apr 7, 2025
8fc6f0b
Merge branch 'fix_deployment_code' into treasury_contracts
dantaik Apr 7, 2025
1565c72
more
dantaik Apr 7, 2025
b52ae47
fix
dantaik Apr 7, 2025
7fe216c
Merge branch 'main' into treasury_contracts
dantaik Apr 7, 2025
b18e9a3
Update MinimalOwner.sol
dantaik Apr 7, 2025
3245086
Update MinimalOwner.sol
dantaik Apr 7, 2025
faf77d8
Update BeaconProofsVerification.t.sol
dantaik Apr 7, 2025
d5ef74f
forge fmt & update contract layout tables
dantaik Apr 7, 2025
4b37f5f
Update gen-layouts.sh
dantaik Apr 7, 2025
fe2c621
forge fmt & update contract layout tables
dantaik Apr 7, 2025
e8c3aa7
Merge branch 'main' into treasury_contracts
dantaik Apr 8, 2025
340552e
more vault tests
Apr 11, 2025
5820933
some more tests
Apr 11, 2025
755b145
fix
dantaik Apr 12, 2025
83a8269
Update MinimalOwner.sol
dantaik Apr 12, 2025
1c1a25a
Update MinimalOwner.t.sol
dantaik Apr 12, 2025
848a575
Update MinimalOwner.t.sol
dantaik Apr 12, 2025
742dd0d
Merge branch 'main' into treasury_contracts
dantaik Apr 13, 2025
7006a46
forge fmt & update contract layout tables
dantaik Apr 13, 2025
c0a20ce
Merge branch 'main' into treasury_contracts
dantaik Apr 14, 2025
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
15 changes: 15 additions & 0 deletions packages/protocol/.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ TestMerkleClaimable:test_verifyClaim_twice_while_its_ongoing() (gas: 66484)
TestMerkleClaimable:test_verifyClaim_when_it_ends() (gas: 61402)
TestMerkleClaimable:test_verifyClaim_when_it_starts() (gas: 61389)
TestMerkleClaimable:test_verifyClaim_with_invalid_proofs_while_its_ongoing() (gas: 48963)
TestMinimalOwner:test_minimalOwner_ForwardCall() (gas: 22790)
TestMinimalOwner:test_minimalOwner_ForwardCallNotOwner() (gas: 20048)
TestMinimalOwner:test_minimalOwner_InitialOwner() (gas: 13060)
TestMinimalOwner:test_minimalOwner_TransferOwnership() (gas: 21853)
TestMinimalOwner:test_minimalOwner_TransferOwnershipToSameAddress() (gas: 13767)
TestMinimalOwner:test_minimalOwner_TransferOwnershipToZeroAddress() (gas: 13429)
TestPreconfWhitelist:test_whitelistNoDelay_consolidationPreservesOrder() (gas: 252745)
TestPreconfWhitelist:test_whitelistNoDelay_consolidationWillNotChangeCurrentEpochOperator() (gas: 299356)
TestPreconfWhitelist:test_whitelist_addBackRemovedOperator() (gas: 106230)
Expand All @@ -107,6 +113,15 @@ TestPreconfWhitelist:test_whitelist_delay2epoch_addThenRemoveTwoOperators() (gas
TestPreconfWhitelist:test_whitelist_noDelay_addThenRemoveOneOperator() (gas: 154524)
TestPreconfWhitelist:test_whitelist_removeNonExistingOperatorWillRevert() (gas: 25210)
TestPreconfWhitelist:test_whitelist_selfRemoval() (gas: 174701)
TestTaikoTreasuryVault:testTreasuryVaultTransferERC20() (gas: 73235)
TestTaikoTreasuryVault:testTreasuryVaultTransferEther() (gas: 69442)
TestTokenLocker:testLock() (gas: 100173)
TestTokenLocker:testLockZeroAmount() (gas: 22559)
TestTokenLocker:testLockedAmount() (gas: 151239)
TestTokenLocker:testUnlock() (gas: 152405)
TestTokenLocker:testUnlockMoreThanUnlocked() (gas: 104381)
TestTokenLocker:testUnlockWithoutInitialization() (gas: 14941)
TestTokenLocker:testUnlockedAmount() (gas: 99639)
TestTokenUnlock:test_tokenunlock_delegate() (gas: 192919)
TestTokenUnlock:test_tokenunlock_multiple_vest_withdrawal() (gas: 200996)
TestTokenUnlock:test_tokenunlock_multiple_vest_withdrawing() (gas: 444309)
Expand Down
29 changes: 29 additions & 0 deletions packages/protocol/contract_layout_layer1.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,3 +1354,32 @@
╰-----------------------------+----------------------------------------------------------+------+--------+-------+---------------------------------------------------------------------╯


## TaikoTreasuryVault

╭-----------------------------+-------------+------+--------+-------+-----------------------------------------------------------------------╮
| Name | Type | Slot | Offset | Bytes |
+===========================================================================================================================================+
| _initialized | uint8 | 0 | 0 | 1 |
|
| _initializing | bool | 0 | 1 | 1 |
|
| __gap | uint256[50] | 1 | 0 | 1600 |
|
| _owner | address | 51 | 0 | 20 |
|
| __gap | uint256[49] | 52 | 0 | 1568 |
|
| _pendingOwner | address | 101 | 0 | 20 |
|
| __gap | uint256[49] | 102 | 0 | 1568 |
|
| __gapFromOldAddressResolver | uint256[50] | 151 | 0 | 1600 |
|
| __reentry | uint8 | 201 | 0 | 1 |
|
| __paused | uint8 | 201 | 1 | 1 |
|
| __gap | uint256[49] | 202 | 0 | 1568 |
╰-----------------------------+-------------+------+--------+-------+-----------------------------------------------------------------------╯


59 changes: 59 additions & 0 deletions packages/protocol/contracts/layer1/governance/MinimalOwner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title MinimalOwner
/// @notice
/// A minimal contract that can:
/// 1) Own other contracts (receive ownership transfers),
/// 2) Transfer ownership (for example, to Taiko DAO),
/// 3) Forward arbitrary calls (execute) to any address, restricted by onlyOwner.
/// @custom:security-contact [email protected]
contract MinimalOwner {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

error NotOwner();
error ZeroAddress();
error SameAddress();
error CallFailed();

address public owner;

modifier onlyOwner() {
require(msg.sender == owner, NotOwner());
_;
}

constructor(address _owner) {
require(_owner != address(0), ZeroAddress());
owner = _owner;
}

/// @notice Transfer ownership of this contract to a new address
/// (e.g. from an EOA to B, or from B to another address).
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), ZeroAddress());
require(newOwner != owner, SameAddress());
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}

/// @notice Forward arbitrary calls to another contract.
/// This lets MinimalOwner directly interact with contracts it owns.
///
/// @param target The contract to call
/// @param data Encoded function call + arguments
/// @return result The raw returned data from the call
function forwardCall(
address target,
bytes calldata data
)
external
payable
onlyOwner
returns (bytes memory result)
{
(bool success, bytes memory returnData) = target.call{ value: msg.value }(data);
require(success, CallFailed());
return returnData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "src/shared/common/EssentialContract.sol";

/// @title TaikoTreasuryVault
/// @notice A contract for managing the Taiko treasury assets. This contract shall be owned by the
/// Taiko DAO.
/// @custom:security-contact [email protected]
contract TaikoTreasuryVault is EssentialContract {
error CallFailed();
error InvalidTarget();

constructor() EssentialContract(address(0)) { }

function init(address _owner) external initializer {
__Essential_init(_owner);
}

// Accept Ether transfers
receive() external payable { }
fallback() external payable { }

/// @notice Executes a low-level call to any target with supplied calldata
/// @param target Address of the contract to call
/// @param value Value to send with the call
/// @param data Calldata (function selector + arguments)
function forwardCall(
address target,
uint256 value,
bytes calldata data
)
external
nonReentrant
onlyOwner
returns (bytes memory)
{
require(target != address(this), InvalidTarget());
(bool success, bytes memory result) = payable(target).call{ value: value }(data);
require(success, CallFailed());
return result;
}
}
72 changes: 72 additions & 0 deletions packages/protocol/contracts/layer1/governance/TokenLocker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title TokenLocker
/// @notice A contract for locking and unlocking tokens with a linear release schedule.
/// @dev This contract regulates the spending rate of TAIKO tokens to prevent the DAO from being
/// exploited by malicious key opinion leaders (KOLs) who might rapidly deplete the TAIKO token
/// treasury.
/// The contract is intentionally designed to be non-upgradable.
/// @custom:security-contact [email protected]
contract TokenLocker is Ownable {
error AlreadyInitialized();
error AmountIsZero();
error InsufficientUnlocked();
error InvalidDuration();
error InvalidRecipient();
error InvalidToken();
error NotInitialized();
error TransferFailed();

IERC20 public immutable token;
uint256 public immutable duration;
uint256 public immutable startTime;
uint256 public immutable endTime;

uint256 public totalLocked;
uint256 public totalUnlocked;

bool public initialized;

constructor(address _token, uint256 _durationYears) {
require(_token != address(0), InvalidToken());
require(_durationYears != 0, InvalidDuration());
token = IERC20(_token);
startTime = block.timestamp;
endTime = startTime + _durationYears * 365 days;
}

function lock(uint256 amount) external onlyOwner {
require(!initialized, AlreadyInitialized());
require(amount != 0, AmountIsZero());

initialized = true;
totalLocked = amount;

require(token.transferFrom(msg.sender, address(this), amount), TransferFailed());
}

function unlock(address recipient, uint256 amount) external onlyOwner {
require(initialized, NotInitialized());
require(recipient != address(0) && recipient != address(this), InvalidRecipient());
require(amount <= unlockedAmount() - totalUnlocked, InsufficientUnlocked());

totalUnlocked += amount;
require(token.transfer(recipient, amount), TransferFailed());
}

function unlockedAmount() public view returns (uint256) {
if (!initialized) return 0;
if (block.timestamp >= endTime) return totalLocked;

uint256 elapsed = block.timestamp - startTime;
return (totalLocked * elapsed) / (endTime - startTime);
}

function lockedAmount() external view returns (uint256) {
return totalLocked - totalUnlocked;
}
}
1 change: 1 addition & 0 deletions packages/protocol/script/gen-layouts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ contracts_layer1=(
"contracts/layer1/forced-inclusion/ForcedInclusionStore.sol:ForcedInclusionStore"
"contracts/layer1/preconf/impl/PreconfRouter.sol:PreconfRouter"
"contracts/layer1/preconf/impl/PreconfWhitelist.sol:PreconfWhitelist"
"contracts/layer1/governance/TaikoTreasuryVault.sol:TaikoTreasuryVault"
)

# Layer 2 contracts
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/script/layer1/hekla/UpdatePacayaL2.s.sol
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
Expand Down
58 changes: 58 additions & 0 deletions packages/protocol/test/layer1/governance/MinimalOwner.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/src/Test.sol";
import "src/layer1/governance/MinimalOwner.sol";

contract TestMinimalOwner is Test {
MinimalOwner minimalOwner;
address owner = address(0x123);
address newOwner = address(0x456);
address target = address(0x789);
bytes data = abi.encodeWithSignature("someFunction()");

function setUp() public {
minimalOwner = new MinimalOwner(owner);
}

function test_minimalOwner_InitialOwner() public view {
assertEq(minimalOwner.owner(), owner, "Owner should be set correctly");
}

function test_minimalOwner_TransferOwnership() public {
vm.startPrank(owner);
minimalOwner.transferOwnership(newOwner);
assertEq(minimalOwner.owner(), newOwner, "Ownership should be transferred");
vm.stopPrank();
}

function test_minimalOwner_TransferOwnershipToZeroAddress() public {
vm.startPrank(owner);
vm.expectRevert(MinimalOwner.ZeroAddress.selector);
minimalOwner.transferOwnership(address(0));
vm.stopPrank();
}

function test_minimalOwner_TransferOwnershipToSameAddress() public {
vm.startPrank(owner);
vm.expectRevert(MinimalOwner.SameAddress.selector);
minimalOwner.transferOwnership(owner);
vm.stopPrank();
}

function test_minimalOwner_ForwardCall() public {
vm.startPrank(owner);
(bool success,) = target.call(data);
require(success);
bytes memory result = minimalOwner.forwardCall(target, data);
assertEq(result, "", "Forwarded call should return correct data");
vm.stopPrank();
}

function test_minimalOwner_ForwardCallNotOwner() public {
vm.startPrank(newOwner);
vm.expectRevert(MinimalOwner.NotOwner.selector);
minimalOwner.forwardCall(target, data);
vm.stopPrank();
}
}
64 changes: 64 additions & 0 deletions packages/protocol/test/layer1/governance/TaikoTreasuryVault.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/src/Test.sol";
import "forge-std/src/console2.sol";
import "src/layer1/governance/TaikoTreasuryVault.sol";
import "test/mocks/TestERC20.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract TestTaikoTreasuryVault is Test {
ERC1967Proxy proxy;
TaikoTreasuryVault vault;
TestERC20 token;
address owner = address(0x123);
address recipient = address(0x456);
uint256 initialBalance = 1000 * 10 ** 18;

function setUp() public {
bytes memory initData = abi.encodeWithSelector(TaikoTreasuryVault.init.selector, owner);

vault = TaikoTreasuryVault(
payable(address(new ERC1967Proxy(address(new TaikoTreasuryVault()), initData)))
);

token = new TestERC20("TestERC20", "TestERC20");
token.mint(owner, initialBalance);

vm.deal(owner, 10 ether);
}

function testTreasuryVaultTransferERC20() public {
vm.startPrank(owner);

// First transfer tokens to the vault
token.transfer(address(vault), initialBalance);
assertEq(token.balanceOf(address(vault)), initialBalance, "Vault should have tokens");
assertEq(token.balanceOf(owner), 0, "Owner should have no tokens left");

// Now use vault to transfer tokens to recipient
bytes memory data =
abi.encodeWithSelector(token.transfer.selector, recipient, initialBalance);
vault.forwardCall(address(token), 0, data);

// Verify final balances
assertEq(token.balanceOf(recipient), initialBalance, "Recipient should have all tokens");
assertEq(token.balanceOf(address(vault)), 0, "Vault should have no tokens left");

vm.stopPrank();
}

function testTreasuryVaultTransferEther() public {
vm.startPrank(owner);

// Send Ether to the vault
(bool success,) = payable(address(vault)).call{ value: 1 ether }("");
require(success, "Transfer failed");

vault.forwardCall(recipient, 1 ether, "");

assertEq(address(vault).balance, 0);
assertEq(recipient.balance, 1 ether);
vm.stopPrank();
}
}
Loading
Loading