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(protocol): add bridge rate limiter for ETH and ERC20s #16970

Merged
merged 42 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f08d003
add IRateLimiter
dantaik May 3, 2024
27b11cb
Update ERC20Vault.sol
dantaik May 3, 2024
5e5d41c
more
dantaik May 3, 2024
9924c27
Update RateLimiter.sol
dantaik May 3, 2024
18f8d10
more
dantaik May 3, 2024
d5d0f5f
rename
dantaik May 3, 2024
458ae3a
Update RateLimiter.sol
dantaik May 3, 2024
1c9539c
Delete RateLimiter.sol
dantaik May 3, 2024
89fe11c
Merge branch 'main' into rate_limiter
dantaik May 3, 2024
8de743e
Update packages/protocol/contracts/common/AddressResolver.sol
dantaik May 3, 2024
b11bed0
Rate limiter impl (#16972)
dantaik May 3, 2024
e481721
Update QuotaManager.sol
dantaik May 3, 2024
daf4d1a
rename
dantaik May 3, 2024
e62940e
rename
dantaik May 3, 2024
c08314f
Update ERC20Vault.sol
dantaik May 3, 2024
6702f92
Merge branch 'main' into rate_limiter
dantaik May 3, 2024
b337bc7
more
dantaik May 3, 2024
df3dcec
Merge branch 'rate_limiter' of https://github.com/taikoxyz/taiko-mono…
dantaik May 3, 2024
919ec43
Update Bridge.sol
dantaik May 3, 2024
453d5f2
Update contract_layout.md
dantaik May 3, 2024
e0d9dca
update
dantaik May 3, 2024
0d197ef
more
dantaik May 3, 2024
d7fd5d8
Add contract layout table
dantaik May 3, 2024
eeb0a24
Merge branch 'main' into rate_limiter
dantaik May 3, 2024
55dea3b
Add contract layout table
dantaik May 3, 2024
9ed91b3
Update QuotaManager.sol
dantaik May 3, 2024
f9dd9d0
more
dantaik May 3, 2024
acffccd
fix
dantaik May 3, 2024
be7e450
Create DeployL1QuotaManager.s.sol
dantaik May 3, 2024
1ef6dc2
Update DeployL1QuotaManager.s.sol
dantaik May 3, 2024
112f4d1
Update DeployL1QuotaManager.s.sol
dantaik May 3, 2024
162f5ab
Update QuotaManager.sol
dantaik May 3, 2024
9621529
Revert "Update contract_layout.md"
dantaik May 4, 2024
1ff1573
Merge branch 'main' into rate_limiter
dantaik May 4, 2024
ecd19da
more
dantaik May 4, 2024
9a349b4
Add contract layout table
dantaik May 4, 2024
4675507
Merge branch 'main' into rate_limiter
dantaik May 4, 2024
ab56273
Update pnpm-lock.yaml
dantaik May 4, 2024
a470f3e
leap
dantaik May 5, 2024
e18586b
fix(protocol): fix two bridge bugs with additional tests (#16992)
dantaik May 6, 2024
766bee3
Merge branch 'main' into rate_limiter
dantaik May 6, 2024
36d6573
Merge branch 'main' into rate_limiter
dantaik May 6, 2024
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
20 changes: 20 additions & 0 deletions packages/protocol/contract_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,23 @@
| isImageTrusted | mapping(bytes32 => bool) | 252 | 0 | 32 | contracts/verifiers/RiscZeroVerifier.sol:RiscZeroVerifier |
| __gap | uint256[48] | 253 | 0 | 1536 | contracts/verifiers/RiscZeroVerifier.sol:RiscZeroVerifier |

## QuotaManager
| Name | Type | Slot | Offset | Bytes | Contract |
|----------------|-----------------------------------------------|------|--------|-------|------------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager |
| _initializing | bool | 0 | 1 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[50] | 1 | 0 | 1600 | contracts/bridge/QuotaManager.sol:QuotaManager |
| _owner | address | 51 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[49] | 52 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager |
| _pendingOwner | address | 101 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[49] | 102 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager |
| addressManager | address | 151 | 0 | 20 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[49] | 152 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __reentry | uint8 | 201 | 0 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __paused | uint8 | 201 | 1 | 1 | contracts/bridge/QuotaManager.sol:QuotaManager |
| lastUnpausedAt | uint64 | 201 | 2 | 8 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[49] | 202 | 0 | 1568 | contracts/bridge/QuotaManager.sol:QuotaManager |
| tokenQuota | mapping(address => struct QuotaManager.Quota) | 251 | 0 | 32 | contracts/bridge/QuotaManager.sol:QuotaManager |
| quotaPeriod | uint24 | 252 | 0 | 3 | contracts/bridge/QuotaManager.sol:QuotaManager |
| __gap | uint256[48] | 253 | 0 | 1536 | contracts/bridge/QuotaManager.sol:QuotaManager |

11 changes: 11 additions & 0 deletions packages/protocol/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "../libs/LibAddress.sol";
import "../libs/LibMath.sol";
import "../signal/ISignalService.sol";
import "./IBridge.sol";
import "./IQuotaManager.sol";

/// @title Bridge
/// @notice See the documentation for {IBridge}.
Expand Down Expand Up @@ -179,6 +180,7 @@ contract Bridge is EssentialContract, IBridge {
if (!received) revert B_SIGNAL_NOT_RECEIVED();

_updateMessageStatus(msgHash, Status.RECALLED);
_consumeEtherQuota(_message.value);

// Execute the recall logic based on the contract's support for the
// IRecallableSender interface
Expand Down Expand Up @@ -219,6 +221,7 @@ contract Bridge is EssentialContract, IBridge {

bytes32 msgHash = hashMessage(_message);
_checkStatus(msgHash, Status.NEW);
_consumeEtherQuota(_message.value + _message.fee);

address signalService = resolve(LibStrings.B_SIGNAL_SERVICE, false);

Expand Down Expand Up @@ -282,6 +285,7 @@ contract Bridge is EssentialContract, IBridge {
{
bytes32 msgHash = hashMessage(_message);
_checkStatus(msgHash, Status.RETRIABLE);
_consumeEtherQuota(_message.value);

uint256 invocationGasLimit;
if (msg.sender != _message.destOwner) {
Expand Down Expand Up @@ -594,4 +598,11 @@ contract Bridge is EssentialContract, IBridge {
function _checkStatus(bytes32 _msgHash, Status _expectedStatus) private view {
if (messageStatus[_msgHash] != _expectedStatus) revert B_INVALID_STATUS();
}

function _consumeEtherQuota(uint256 _amount) private {
address quotaManager = resolve(LibStrings.B_QUOTA_MANAGER, true);
if (quotaManager != address(0)) {
IQuotaManager(quotaManager).consumeQuota(address(0), _amount);
}
}
}
12 changes: 12 additions & 0 deletions packages/protocol/contracts/bridge/IQuotaManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @title IQuotaManager
/// @custom:security-contact security@taiko.xyz
interface IQuotaManager {
/// @notice Consumes a specific amount of quota for a given address.
/// This function must revert if available quota is smaller than the given amount of quota.
/// @param _token The token address. Ether is represented with address(0).
/// @param _amount The amount of quota to consume.
function consumeQuota(address _token, uint256 _amount) external;
}
90 changes: 90 additions & 0 deletions packages/protocol/contracts/bridge/QuotaManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../common/EssentialContract.sol";
import "../common/LibStrings.sol";
import "../libs/LibMath.sol";
import "./IQuotaManager.sol";

/// @title QuotaManager
/// @dev An implementation of IQuotaManager for Ether and ERC20 tokens.
/// @custom:security-contact security@taiko.xyz
contract QuotaManager is EssentialContract, IQuotaManager {
using LibMath for uint256;

struct Quota {
uint48 updatedAt;
uint104 quota;
uint104 available;
}

mapping(address token => Quota tokenLimit) public tokenQuota;
uint24 public quotaPeriod;

uint256[48] private __gap;

event QuotaUpdated(address indexed token, uint256 oldQuota, uint256 newQuota);

error QM_INVALID_PARAM();
error QM_OUT_OF_QUOTA();

/// @notice Initializes the contract.
/// @param _owner The owner of this contract. msg.sender will be used if this value is zero.
/// @param _addressManager The address of the {AddressManager} contract.
/// @param _quotaPeriod The time required to restore all quota.
function init(
address _owner,
address _addressManager,
uint24 _quotaPeriod
)
external
initializer
{
if (_quotaPeriod == 0) revert QM_INVALID_PARAM();

__Essential_init(_owner, _addressManager);
quotaPeriod = _quotaPeriod;
}

/// @notice Updates the daily quota for a given address.
/// @param _token The token address with Ether represented by address(0).
/// @param _quota The new daily quota.
function updateQuota(address _token, uint104 _quota) external onlyOwner whenNotPaused {
if (_quota == tokenQuota[_token].quota) revert QM_INVALID_PARAM();

emit QuotaUpdated(_token, tokenQuota[_token].quota, _quota);
tokenQuota[_token].quota = _quota;
}

/// @inheritdoc IQuotaManager
function consumeQuota(
address _token,
uint256 _amount
)
external
whenNotPaused
onlyFromNamedEither(LibStrings.B_BRIDGE, LibStrings.B_ERC20_VAULT)
{
uint256 available = availableQuota(_token);
if (available == type(uint256).max) return;
if (available < _amount) revert QM_OUT_OF_QUOTA();

unchecked {
available -= _amount;
}
tokenQuota[_token].available = uint104(available);
tokenQuota[_token].updatedAt = uint48(block.timestamp);
}

/// @notice Returns the available quota for a given token.
/// @param _token The token address with Ether represented by address(0).
/// @return The available quota.
function availableQuota(address _token) public view returns (uint256) {
Quota memory q = tokenQuota[_token];
if (q.quota == 0) return type(uint256).max;
if (q.updatedAt == 0) return q.quota;

uint256 issuance = q.quota * (block.timestamp - q.updatedAt) / quotaPeriod;
return (issuance + q.available).min(q.quota);
}
}
11 changes: 11 additions & 0 deletions packages/protocol/contracts/common/AddressResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ abstract contract AddressResolver is IAddressResolver, Initializable {
_;
}

/// @dev Modifier that ensures the caller is a resolved address to either _name1 or _name2
/// name.
/// @param _name1 The first name to check against.
/// @param _name2 The second name to check against.
modifier onlyFromNamedEither(bytes32 _name1, bytes32 _name2) {
if (msg.sender != resolve(_name1, true) && msg.sender != resolve(_name2, true)) {
revert RESOLVER_DENIED();
}
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand Down
34 changes: 17 additions & 17 deletions packages/protocol/contracts/common/LibStrings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ pragma solidity 0.8.24;
/// @title LibStrings
/// @custom:security-contact security@taiko.xyz
library LibStrings {
bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog");
bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer");
bytes32 internal constant B_PROPOSER = bytes32("proposer");
bytes32 internal constant B_PROPOSER_ONE = bytes32("proposer_one");
bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service");
bytes32 internal constant B_TAIKO_TOKEN = bytes32("taiko_token");
bytes32 internal constant B_TAIKO = bytes32("taiko");
bytes32 internal constant B_AUTOMATA_DCAP_ATTESTATION = bytes32("automata_dcap_attestation");
dantaik marked this conversation as resolved.
Show resolved Hide resolved
bytes32 internal constant B_BRIDGE = bytes32("bridge");
bytes32 internal constant B_ERC20_VAULT = bytes32("erc20_vault");
bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog");
bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155");
bytes32 internal constant B_BRIDGED_ERC20 = bytes32("bridged_erc20");
bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721");
bytes32 internal constant B_CHAIN_WATCHDOG = bytes32("chain_watchdog");
bytes32 internal constant B_ERC1155_VAULT = bytes32("erc1155_vault");
bytes32 internal constant B_BRIDGED_ERC1155 = bytes32("bridged_erc1155");
bytes32 internal constant B_ERC20_VAULT = bytes32("erc20_vault");
bytes32 internal constant B_ERC721_VAULT = bytes32("erc721_vault");
bytes32 internal constant B_BRIDGED_ERC721 = bytes32("bridged_erc721");
bytes32 internal constant B_BRIDGE_WATCHDOG = bytes32("bridge_watchdog");
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_QUOTA_MANAGER = bytes32("quota_manager");
bytes32 internal constant B_SGX_WATCHDOG = bytes32("sgx_watchdog");
bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service");
bytes32 internal constant B_TAIKO = bytes32("taiko");
bytes32 internal constant B_TAIKO_TOKEN = bytes32("taiko_token");
bytes32 internal constant B_TIER_GUARDIAN = bytes32("tier_guardian");
bytes32 internal constant B_TIER_GUARDIAN_MINORITY = bytes32("tier_guardian_minority");
bytes32 internal constant B_TIER_PROVIDER = bytes32("tier_provider");
bytes32 internal constant B_TIER_SGX = bytes32("tier_sgx");
bytes32 internal constant B_TIER_SGX_ZKVM = bytes32("tier_sgx_zkvm");
bytes32 internal constant B_TIER_GUARDIAN_MINORITY = bytes32("tier_guardian_minority");
bytes32 internal constant B_TIER_GUARDIAN = bytes32("tier_guardian");
bytes32 internal constant B_AUTOMATA_DCAP_ATTESTATION = bytes32("automata_dcap_attestation");
bytes32 internal constant B_PROVER_ASSIGNMENT = bytes32("PROVER_ASSIGNMENT");
bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer");
bytes32 internal constant H_RETURN_LIVENESS_BOND = keccak256("RETURN_LIVENESS_BOND");
bytes32 internal constant H_STATE_ROOT = keccak256("STATE_ROOT");
bytes32 internal constant H_SIGNAL_ROOT = keccak256("SIGNAL_ROOT");
string internal constant S_SIGNAL = "SIGNAL";
bytes32 internal constant H_STATE_ROOT = keccak256("STATE_ROOT");
}
2 changes: 1 addition & 1 deletion packages/protocol/contracts/signal/SignalService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ contract SignalService is EssentialContract, ISignalService {
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(LibStrings.S_SIGNAL, _chainId, _app, _signal));
return keccak256(abi.encodePacked("SIGNAL", _chainId, _app, _signal));
}

function _verifyHopProof(
Expand Down
10 changes: 10 additions & 0 deletions packages/protocol/contracts/tokenvault/ERC20Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../bridge/IQuotaManager.sol";
import "../libs/LibAddress.sol";
import "./BridgedERC20.sol";
import "./BaseVault.sol";
Expand Down Expand Up @@ -336,6 +337,8 @@ contract ERC20Vault is BaseVault {
// check.
IBridgedERC20(token_).mint(_to, _amount);
}

_consumeTokenQuota(token_, _amount);
}

/// @dev Handles the message on the source chain and returns the encoded
Expand Down Expand Up @@ -442,4 +445,11 @@ contract ERC20Vault is BaseVault {
ctokenDecimal: ctoken.decimals
});
}

function _consumeTokenQuota(address _token, uint256 _amount) private {
address quotaManager = resolve(LibStrings.B_QUOTA_MANAGER, true);
if (quotaManager != address(0)) {
IQuotaManager(quotaManager).consumeQuota(_token, _amount);
}
}
}
1 change: 1 addition & 0 deletions packages/protocol/deployments/gen-layouts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ contracts=(
"AutomataDcapV3Attestation"
"SgxVerifier"
"RiscZeroVerifier"
"QuotaManager"
)

# Empty the output file initially
Expand Down
36 changes: 36 additions & 0 deletions packages/protocol/script/DeployL1QuotaManager.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../test/DeployCapability.sol";
import "../contracts/bridge/QuotaManager.sol";

contract DeployL1QuotaManager is DeployCapability {
uint256 public privateKey = vm.envUint("PRIVATE_KEY");
// MAINNET_SECURITY_COUNCIL: council.taiko.eth (0x7C50d60743D3FCe5a39FdbF687AFbAe5acFF49Fd)
address public addressManager = vm.envAddress("L1_ROLLUP_ADDRESS_MANAGER");
address public owner = vm.envAddress("OWNER");

modifier broadcast() {
require(privateKey != 0, "invalid private key");
vm.startBroadcast();
_;
vm.stopBroadcast();
}

function run() external broadcast {
// Deploy the QuotaManager contract with a 15 minute quota period
QuotaManager qm = QuotaManager(
deployProxy({
name: "quota_manager",
impl: address(new QuotaManager()),
data: abi.encodeCall(QuotaManager.init, (owner, addressManager, 15 minutes))
})
);

// L2-to-L1 Ether per 15 minutes: 500 Ether
qm.updateQuota(address(0), 250 ether);

// L2-to-L1 TKO per 15 minutes: 100_000 (0.01% total supply)
qm.updateQuota(0x10dea67478c5F8C5E2D90e5E9B26dBe60c54d800, 100_000 ether);
}
}
1 change: 1 addition & 0 deletions packages/protocol/test/TaikoTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import "../contracts/verifiers/RiscZeroVerifier.sol";
import "../contracts/L1/tiers/TierProviderV1.sol";
import "../contracts/L1/hooks/AssignmentHook.sol";
import "../contracts/L1/provers/GuardianProver.sol";
import "../contracts/bridge/QuotaManager.sol";

import "../contracts/L2/DelegateOwner.sol";

Expand Down
Loading