diff --git a/packages/protocol/contracts/L1/TaikoData.sol b/packages/protocol/contracts/L1/TaikoData.sol index 5280e278a2..1bf8e33630 100644 --- a/packages/protocol/contracts/L1/TaikoData.sol +++ b/packages/protocol/contracts/L1/TaikoData.sol @@ -140,7 +140,7 @@ library TaikoData { uint64 genesisHeight; uint64 genesisTimestamp; uint64 lastSyncedBlockId; - uint64 lastSynecdAt; + uint64 lastSynecdAt; // typo! } struct SlotB { @@ -149,7 +149,7 @@ library TaikoData { bool provingPaused; uint8 __reservedB1; uint16 __reservedB2; - uint32 __reservedB3; + uint32 lastSnapshotIdx; uint64 lastUnpausedAt; } @@ -164,7 +164,6 @@ library TaikoData { uint64 blockId_mod_blockRingBufferSize => mapping(uint32 transitionId => TransitionState ts) ) transitions; - // Ring buffer for Ether deposits bytes32 __reserve1; SlotA slotA; // slot 5 SlotB slotB; // slot 6 diff --git a/packages/protocol/contracts/L1/TaikoEvents.sol b/packages/protocol/contracts/L1/TaikoEvents.sol index 60b877443e..f342fb058a 100644 --- a/packages/protocol/contracts/L1/TaikoEvents.sol +++ b/packages/protocol/contracts/L1/TaikoEvents.sol @@ -45,6 +45,12 @@ abstract contract TaikoEvents { /// @param slotB The SlotB data structure. event StateVariablesUpdated(TaikoData.SlotB slotB); + /// @notice Emitted when the Taiko token snapshot is taken. + /// @param tkoAddress The Taiko token address. + /// @param snapshotIdx The snapshot index. + /// @param snapshotId The snapshot id. + event TaikoTokenSnapshot(address tkoAddress, uint256 snapshotIdx, uint256 snapshotId); + /// @dev Emitted when a block transition is proved or re-proved. /// @param blockId The ID of the proven block. /// @param tran The verified transition. diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol index ac450b627c..bb1b3579e5 100644 --- a/packages/protocol/contracts/L1/TaikoL1.sol +++ b/packages/protocol/contracts/L1/TaikoL1.sol @@ -59,7 +59,6 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents, TaikoErrors { // reset some previously used slots for future reuse state.slotB.__reservedB1 = 0; state.slotB.__reservedB2 = 0; - state.slotB.__reservedB3 = 0; state.__reserve1 = 0; } diff --git a/packages/protocol/contracts/L1/TaikoToken.sol b/packages/protocol/contracts/L1/TaikoToken.sol index c02a102351..0a29fde763 100644 --- a/packages/protocol/contracts/L1/TaikoToken.sol +++ b/packages/protocol/contracts/L1/TaikoToken.sol @@ -53,7 +53,7 @@ contract TaikoToken is EssentialContract, ERC20SnapshotUpgradeable, ERC20VotesUp } /// @notice Creates a new token snapshot. - function snapshot() public onlyFromOwnerOrNamed(LibStrings.B_SNAPSHOOTER) returns (uint256) { + function snapshot() public onlyFromNamed(LibStrings.B_TAIKO) returns (uint256) { return _snapshot(); } @@ -84,6 +84,10 @@ contract TaikoToken is EssentialContract, ERC20SnapshotUpgradeable, ERC20VotesUp return super.transferFrom(_from, _to, _amount); } + function currentSnapshotId() public view returns (uint256) { + return _getCurrentSnapshotId(); + } + function _beforeTokenTransfer( address _from, address _to, diff --git a/packages/protocol/contracts/L1/libs/LibProposing.sol b/packages/protocol/contracts/L1/libs/LibProposing.sol index 61ad57d699..fee0a802ad 100644 --- a/packages/protocol/contracts/L1/libs/LibProposing.sol +++ b/packages/protocol/contracts/L1/libs/LibProposing.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../common/IAddressResolver.sol"; -import "../../common/LibStrings.sol"; +import "../../common/LibSnapshot.sol"; import "../../libs/LibAddress.sol"; import "../../libs/LibNetwork.sol"; import "../hooks/IHook.sol"; @@ -185,6 +185,8 @@ library LibProposing { { IERC20 tko = IERC20(_resolver.resolve(LibStrings.B_TAIKO_TOKEN, false)); + _takeTaikoTokenSnapshot(_state, address(tko), b); + uint256 tkoBalance = tko.balanceOf(address(this)); // Run all hooks. @@ -230,6 +232,19 @@ library LibProposing { }); } + function _takeTaikoTokenSnapshot( + TaikoData.State storage _state, + address _taikoToken, + TaikoData.SlotB memory _slotB + ) + private + { + uint32 idx = LibSnapshot.autoSnapshot(_taikoToken, block.number, _slotB.lastSnapshotIdx); + if (idx != 0) { + _state.slotB.lastSnapshotIdx = idx; + } + } + function _isProposerPermitted( TaikoData.SlotB memory _slotB, IAddressResolver _resolver diff --git a/packages/protocol/contracts/L2/TaikoL2.sol b/packages/protocol/contracts/L2/TaikoL2.sol index e0d36a4f97..19893cd8b9 100644 --- a/packages/protocol/contracts/L2/TaikoL2.sol +++ b/packages/protocol/contracts/L2/TaikoL2.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../common/EssentialContract.sol"; -import "../common/LibStrings.sol"; +import "../common/LibSnapshot.sol"; import "../libs/LibAddress.sol"; import "../signal/ISignalService.sol"; import "./Lib1559Math.sol"; @@ -47,8 +47,11 @@ contract TaikoL2 is EssentialContract { uint64 private __currentBlockTimestamp; /// @notice The L1's chain ID. + /// @dev Slot 4. uint64 public l1ChainId; + uint32 public lastSnapshotIdx; + uint256[46] private __gap; /// @notice Emitted when the latest L1 block details are anchored to L2. @@ -56,6 +59,12 @@ contract TaikoL2 is EssentialContract { /// @param gasExcess The gas excess value used to calculate the base fee. event Anchored(bytes32 parentHash, uint64 gasExcess); + /// @notice Emitted when the Taiko token snapshot is taken. + /// @param tkoAddress The Taiko token address. + /// @param snapshotIdx The snapshot index. + /// @param snapshotId The snapshot id. + event TaikoTokenSnapshot(address tkoAddress, uint256 snapshotIdx, uint256 snapshotId); + error L2_BASEFEE_MISMATCH(); error L2_INVALID_L1_CHAIN_ID(); error L2_INVALID_L2_CHAIN_ID(); @@ -105,6 +114,7 @@ contract TaikoL2 is EssentialContract { /// @notice Anchors the latest L1 block details to L2 for cross-layer /// message verification. + /// @dev The gas limit for this transaction is set to 250K in geth and raiko. /// @dev This function can be called freely as the golden touch private key is publicly known, /// but the Taiko node guarantees the first transaction of each block is always this anchor /// transaction, and any subsequent calls will revert with L2_PUBLIC_INPUT_HASH_MISMATCH. @@ -168,6 +178,12 @@ contract TaikoL2 is EssentialContract { __currentBlockTimestamp = uint64(block.timestamp); gasExcess = _gasExcess; + address tko = resolve(LibStrings.B_TAIKO_TOKEN, true); + if (tko != address(0)) { + uint32 idx = LibSnapshot.autoSnapshot(tko, _l1BlockId, lastSnapshotIdx); + if (idx != 0) lastSnapshotIdx = idx; + } + emit Anchored(_parentHash, _gasExcess); } diff --git a/packages/protocol/contracts/common/LibSnapshot.sol b/packages/protocol/contracts/common/LibSnapshot.sol new file mode 100644 index 0000000000..77ea1a12ab --- /dev/null +++ b/packages/protocol/contracts/common/LibSnapshot.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./IAddressResolver.sol"; +import "./LibStrings.sol"; + +/// @title ISnapshot +/// @custom:security-contact security@taiko.xyz +interface ISnapshot { + function snapshot() external returns (uint256); +} + +/// @title LibSnapshot +/// @custom:security-contact security@taiko.xyz +library LibSnapshot { + uint256 public constant SNAPSHOT_INTERVAL = 7200; // uint = 1 L1 block + + /// @notice Emitted when the Taiko token snapshot is taken. + /// @param tkoAddress The Taiko token address. + /// @param snapshotIdx The snapshot index. + /// @param snapshotId The snapshot id. + event TaikoTokenSnapshot(address tkoAddress, uint256 snapshotIdx, uint256 snapshotId); + + /// @dev Takes a snapshot every 200,000 L1 blocks which is roughly 27 days. + /// @param _taikoToken The Taiko token address. + /// @param _blockId The L1's block ID. + /// @param _lastSnapshotIdx The latest snapshot's index. + /// @return The new snapshot's index, 0 if no new snapshot is taken. + function autoSnapshot( + address _taikoToken, + uint256 _blockId, + uint64 _lastSnapshotIdx + ) + internal + returns (uint32) + { + if (_blockId % SNAPSHOT_INTERVAL != 0) return 0; + + // if snapshotIdx = type(uint32).max, we can handle L1 block id up to 4e14. + uint32 snapshotIdx = uint32(_blockId / SNAPSHOT_INTERVAL + 1); + if (snapshotIdx == _lastSnapshotIdx) return 0; + + uint256 snapshotId = ISnapshot(_taikoToken).snapshot(); + emit TaikoTokenSnapshot(_taikoToken, snapshotIdx, snapshotId); + return snapshotIdx; + } +} diff --git a/packages/protocol/contracts/common/LibStrings.sol b/packages/protocol/contracts/common/LibStrings.sol index 2467e8f217..16a05bf882 100644 --- a/packages/protocol/contracts/common/LibStrings.sol +++ b/packages/protocol/contracts/common/LibStrings.sol @@ -7,9 +7,6 @@ library LibStrings { /// @notice bytes32 representation of the string "chain_pauser". bytes32 internal constant B_CHAIN_PAUSER = bytes32("chain_pauser"); - /// @notice bytes32 representation of the string "snapshooter". - bytes32 internal constant B_SNAPSHOOTER = bytes32("snapshooter"); - /// @notice bytes32 representation of the string "withdrawer". bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer"); diff --git a/packages/protocol/contracts/tokenvault/BridgedERC20.sol b/packages/protocol/contracts/tokenvault/BridgedERC20.sol index 837f5ede72..10c15e1b96 100644 --- a/packages/protocol/contracts/tokenvault/BridgedERC20.sol +++ b/packages/protocol/contracts/tokenvault/BridgedERC20.sol @@ -33,10 +33,8 @@ contract BridgedERC20 is error BTOKEN_CANNOT_RECEIVE(); error BTOKEN_UNAUTHORIZED(); - modifier onlyOwnerOrSnapshooter() { - if (msg.sender != owner() && msg.sender != snapshooter) { - revert BTOKEN_UNAUTHORIZED(); - } + modifier onlyAuthorizedForSnapshot() { + if (!isAuthorizedForSnapshot(msg.sender)) revert BTOKEN_UNAUTHORIZED(); _; } @@ -81,7 +79,7 @@ contract BridgedERC20 is } /// @notice Creates a new token snapshot. - function snapshot() external onlyOwnerOrSnapshooter returns (uint256) { + function snapshot() external onlyAuthorizedForSnapshot returns (uint256) { return _snapshot(); } @@ -118,6 +116,28 @@ contract BridgedERC20 is return __srcDecimals; } + /// @notice Gets the current snapshot ID. + /// @return The current snapshot ID. + function currentSnapshotId() public view returns (uint256) { + return _getCurrentSnapshotId(); + } + + /// @notice Checks if an address can take a snapshot. + /// @param addr The address. + /// @return true if the address can perform a snapshot, false otherwise. + function isAuthorizedForSnapshot(address addr) public view returns (bool) { + if (addr == address(0)) return false; + + if ( + addr == resolve(LibStrings.B_TAIKO, true) + && address(this) == resolve(LibStrings.B_TAIKO_TOKEN, true) + ) return true; + + if (addr == snapshooter) return true; + + return false; + } + /// @notice Gets the canonical token's address and chain ID. /// @return The canonical token's address. /// @return The canonical token's chain ID. diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 943deaff1e..c49cd226b4 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -30,7 +30,7 @@ "eslint-plugin-promise": "^6.1.1", "ethers": "^5.7.2", "solc": "0.7.3", - "solhint": "^4.5.2", + "solhint": "^4.5.4", "ts-node": "^10.9.2", "typescript": "^5.2.2" }, diff --git a/packages/protocol/test/L1/TaikoL1.t.sol b/packages/protocol/test/L1/TaikoL1.t.sol index 1c4c2927da..3516ea22ec 100644 --- a/packages/protocol/test/L1/TaikoL1.t.sol +++ b/packages/protocol/test/L1/TaikoL1.t.sol @@ -187,7 +187,7 @@ contract TaikoL1Test is TaikoL1TestBase { } function test_snapshot() external { - vm.prank(tko.owner(), tko.owner()); + vm.prank(address(L1)); tko.snapshot(); uint256 totalSupplyAtSnapshot = tko.totalSupplyAt(1); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ccdc710fd..973cd0f5fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,8 +328,8 @@ importers: specifier: 0.7.3 version: 0.7.3 solhint: - specifier: ^4.5.2 - version: 4.5.2(typescript@5.4.3) + specifier: ^4.5.4 + version: 4.5.4(typescript@5.4.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.11.20)(typescript@5.4.3) @@ -12242,8 +12242,8 @@ packages: - debug dev: true - /solhint@4.5.2(typescript@5.4.3): - resolution: {integrity: sha512-o7MNYS5QPgE6l+PTGOTAUtCzo0ZLnffQsv586hntSHBe2JbSDfkoxfhAOcjZjN4OesTgaX4UEEjCjH9y/4BP5w==} + /solhint@4.5.4(typescript@5.4.3): + resolution: {integrity: sha512-Cu1XiJXub2q1eCr9kkJ9VPv1sGcmj3V7Zb76B0CoezDOB9bu3DxKIFFH7ggCl9fWpEPD6xBmRLfZrYijkVmujQ==} hasBin: true dependencies: '@solidity-parser/parser': 0.18.0