Repository: celer-network/sgn-v2-contracts
Branch: main
Commit: b8a27161e0b7
Files: 236
Total size: 794.0 KB
Directory structure:
gitextract_l6jfhm9x/
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── abigen.yml
│ ├── pbsol.yml
│ └── run_tests.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── benchmark/
│ └── relay.ts
├── contracts/
│ ├── circle-usdc/
│ │ ├── CircleBridgeProxy.sol
│ │ ├── CircleBridgeProxyV2.sol
│ │ └── FeeOperator.sol
│ ├── governed-owner/
│ │ ├── GovernedOwnerProxy.sol
│ │ ├── SimpleGovernance.sol
│ │ ├── customized/
│ │ │ └── MessageBusOwner.sol
│ │ ├── interfaces/
│ │ │ ├── IBridgeOwner.sol
│ │ │ ├── ICommonOwner.sol
│ │ │ ├── IMessageOwner.sol
│ │ │ ├── ISgnOwner.sol
│ │ │ └── IUpgradeableOwner.sol
│ │ └── proxies/
│ │ ├── BridgeOwnerProxy.sol
│ │ ├── CommonOwnerProxy.sol
│ │ ├── MessageOwnerProxy.sol
│ │ ├── OwnerDataTypes.sol
│ │ ├── OwnerProxyBase.sol
│ │ ├── SgnOwnerProxy.sol
│ │ └── UpgradeableOwnerProxy.sol
│ ├── integration-examples/
│ │ ├── ContractAsLP.sol
│ │ └── ContractAsSender.sol
│ ├── interfaces/
│ │ ├── IBridge.sol
│ │ ├── ICircleBridge.sol
│ │ ├── IDelayedTransfer.sol
│ │ ├── IOriginalTokenVault.sol
│ │ ├── IOriginalTokenVaultV2.sol
│ │ ├── IPeggedToken.sol
│ │ ├── IPeggedTokenBridge.sol
│ │ ├── IPeggedTokenBridgeV2.sol
│ │ ├── IPeggedTokenBurnFrom.sol
│ │ ├── IPool.sol
│ │ ├── ISigsVerifier.sol
│ │ ├── IUniswapV2.sol
│ │ ├── IWETH.sol
│ │ └── IWithdrawInbox.sol
│ ├── libraries/
│ │ ├── BridgeTransferLib.sol
│ │ ├── Pb.sol
│ │ ├── PbBridge.sol
│ │ ├── PbFarming.sol
│ │ ├── PbPegged.sol
│ │ ├── PbPool.sol
│ │ ├── PbSgn.sol
│ │ ├── PbStaking.sol
│ │ ├── Utils.sol
│ │ └── proto/
│ │ ├── README.md
│ │ ├── bridge.proto
│ │ ├── farming.proto
│ │ ├── pegged.proto
│ │ ├── pool.proto
│ │ ├── sgn.proto
│ │ └── staking.proto
│ ├── liquidity-bridge/
│ │ ├── Bridge.sol
│ │ ├── FarmingRewards.sol
│ │ ├── Pool.sol
│ │ ├── Signers.sol
│ │ └── WithdrawInbox.sol
│ ├── message/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── apps/
│ │ │ ├── RFQ.sol
│ │ │ ├── adapter/
│ │ │ │ └── MessageReceiverAdapter.sol
│ │ │ ├── examples/
│ │ │ │ ├── BatchTransfer.sol
│ │ │ │ ├── MsgExampleBasic.sol
│ │ │ │ ├── MsgExampleBasicTransfer.sol
│ │ │ │ ├── MsgExampleInOrder.sol
│ │ │ │ ├── MsgTest.sol
│ │ │ │ ├── TransferSwap.sol
│ │ │ │ └── TransferSwapSendBack.sol
│ │ │ └── nft-bridge/
│ │ │ ├── MCNNFT.sol
│ │ │ ├── NFTBridge.sol
│ │ │ ├── OrigNFT.sol
│ │ │ └── PegNFT.sol
│ │ ├── framework/
│ │ │ ├── MessageApp.sol
│ │ │ ├── MessageBusAddress.sol
│ │ │ ├── MessageReceiverApp.sol
│ │ │ └── MessageSenderApp.sol
│ │ ├── interfaces/
│ │ │ ├── IMessageBus.sol
│ │ │ └── IMessageReceiverApp.sol
│ │ ├── libraries/
│ │ │ ├── MessageSenderLib.sol
│ │ │ └── MsgDataTypes.sol
│ │ ├── messagebus/
│ │ │ ├── MessageBus.sol
│ │ │ ├── MessageBusReceiver.sol
│ │ │ └── MessageBusSender.sol
│ │ └── safeguard/
│ │ ├── DelayedMessage.sol
│ │ └── MessageAppPauser.sol
│ ├── miscs/
│ │ ├── Faucet.sol
│ │ ├── MintableERC20.sol
│ │ ├── oasys/
│ │ │ ├── L1StandardERC20.sol
│ │ │ ├── L1StandardERC20Factory.sol
│ │ │ └── README.md
│ │ └── proxy-admin/
│ │ ├── Ownable.sol
│ │ └── ProxyAdmin.sol
│ ├── pegged-bridge/
│ │ ├── OriginalTokenVault.sol
│ │ ├── OriginalTokenVaultV2.sol
│ │ ├── PeggedBrc20Bridge.sol
│ │ ├── PeggedTokenBridge.sol
│ │ ├── PeggedTokenBridgeV2.sol
│ │ ├── README.md
│ │ ├── customized/
│ │ │ └── PeggedNativeTokenBridge.sol
│ │ └── tokens/
│ │ ├── ERC20Permit/
│ │ │ ├── MintSwapCanonicalTokenPermit.sol
│ │ │ ├── MultiBridgeTokenPermit.sol
│ │ │ └── SingleBridgeTokenPermit.sol
│ │ ├── IntermediaryBridgeToken.sol
│ │ ├── IntermediaryOriginalToken.sol
│ │ ├── MintSwapCanonicalToken.sol
│ │ ├── MintSwapCanonicalTokenUpgradable.sol
│ │ ├── MultiBridgeToken.sol
│ │ ├── SingleBridgeToken.sol
│ │ ├── SwapBridgeToken.sol
│ │ ├── WrappedBridgeToken.sol
│ │ ├── customized/
│ │ │ ├── CircleBridgeToken.sol
│ │ │ ├── FraxBridgeToken.sol
│ │ │ ├── MaiBridgeToken.sol
│ │ │ ├── OntologyBridgeToken.sol
│ │ │ └── xc20/
│ │ │ ├── XC20BridgeHub.sol
│ │ │ ├── XC20BridgeToken.sol
│ │ │ └── interfaces/
│ │ │ └── IXC20BridgeHub.sol
│ │ ├── freezable/
│ │ │ ├── Freezable.sol
│ │ │ ├── MintSwapCanonicalTokenFreezable.sol
│ │ │ └── MintSwapCanonicalTokenUpgradableFreezable.sol
│ │ └── owners/
│ │ └── RestrictedMultiBridgeTokenOwner.sol
│ ├── proxy/
│ │ └── TransferAgent.sol
│ ├── safeguard/
│ │ ├── DelayedTransfer.sol
│ │ ├── Governor.sol
│ │ ├── Ownable.sol
│ │ ├── Pauser.sol
│ │ ├── VolumeControl.sol
│ │ ├── Whitelist.sol
│ │ └── sentinel/
│ │ ├── Guard.sol
│ │ ├── GuardedGovernor.sol
│ │ ├── GuardedPauser.sol
│ │ └── Sentinel.sol
│ ├── staking/
│ │ ├── DataTypes.sol
│ │ ├── Govern.sol
│ │ ├── SGN.sol
│ │ ├── Staking.sol
│ │ ├── StakingReward.sol
│ │ └── Viewer.sol
│ └── test-helpers/
│ ├── DummySwap.sol
│ ├── TestERC20.sol
│ └── WETH.sol
├── deploy/
│ ├── circle-usdc/
│ │ ├── 000_circle_bridge_proxy.ts
│ │ └── 000_circle_bridge_proxy_v2.ts
│ ├── core/
│ │ └── 000_sgn_staking.ts
│ ├── governed-owner/
│ │ ├── 000_governed_owner.ts
│ │ └── customized/
│ │ └── 000_message_bus_owner.ts
│ ├── liquidity-bridge/
│ │ ├── 000_bridge.ts
│ │ └── 001_farming_rewards.ts
│ ├── message/
│ │ ├── 000_message_bus_init.ts
│ │ ├── 001_message_bus.ts
│ │ └── apps/
│ │ ├── 000_transfer_swap.ts
│ │ ├── 001_nft_bridge.ts
│ │ ├── 002_peg_nft.ts
│ │ ├── 003_orig_nft.ts
│ │ ├── 004_mcn_nft.ts
│ │ ├── 005_msg_test.ts
│ │ ├── 006_rfq.ts
│ │ └── 007_adapter.ts
│ ├── miscs/
│ │ ├── 000_test_token.ts
│ │ ├── 001_faucet.ts
│ │ ├── 002_dummy_swap.ts
│ │ ├── 003_withdraw_inbox.ts
│ │ ├── 004_contract_as_lp.ts
│ │ ├── 005_contract_as_sender.ts
│ │ ├── 006_transfer_agent.ts
│ │ └── 007_default_proxy_admin.ts
│ ├── pegged-bridge/
│ │ ├── 000_original_token_vault.ts
│ │ ├── 001_pegged_token_bridge.ts
│ │ ├── 002_original_token_vault_v2.ts
│ │ ├── 003_pegged_token_bridge_v2.ts
│ │ ├── 004_pegged_brc20_bridge.ts
│ │ ├── customized/
│ │ │ └── 000_pegged_native_token_bridge.ts
│ │ └── tokens/
│ │ ├── 000_single_bridge_token.ts
│ │ ├── 001_single_bridge_token_permit.ts
│ │ ├── 002_multi_bridge_token.ts
│ │ ├── 003_multi_bridge_token_permit.ts
│ │ ├── 004_mint_swap_canonical_token.ts
│ │ ├── 005_mint_swap_canonical_token_permit.ts
│ │ ├── 006_mint_swap_canonical_token_upgradable.ts
│ │ ├── 007_wrapped_bridge_token.ts
│ │ ├── 008_intermediary_bridge_token.ts
│ │ ├── 009_intermediary_original_token.ts
│ │ ├── 010_circle_bridge_token.ts
│ │ ├── customized/
│ │ │ ├── 000_frax_bridge_token.ts
│ │ │ ├── 001_mai_bridge_token.ts
│ │ │ └── 002_ontology_bridge_token.ts
│ │ ├── freezable/
│ │ │ ├── 000_mint_swap_canonical_token_upgradable_freezable.ts
│ │ │ └── 001_mint_swap_canonical_token_freezable.ts
│ │ └── owners/
│ │ └── 000_restricted_multi_bridge_token_owner.ts
│ └── sentinel/
│ ├── 000_sentinel.ts
│ └── 001_sentinel_zksync.ts
├── hardhat.config.ts
├── import-sorter.json
├── package.json
├── reports/
│ ├── contract_sizes.txt
│ └── gas_usage/
│ ├── relay.txt
│ └── summary.txt
├── scripts/
│ ├── common.ts
│ ├── init_governed_owner.ts
│ ├── oasys_token_factory.ts
│ ├── pb_gen_sol.sh
│ ├── reset_signers.ts
│ ├── sentinel/
│ │ ├── sentinel_guard.ts
│ │ ├── sentinel_relax.ts
│ │ └── sentinel_set_limits.ts
│ ├── set_basics.ts
│ ├── set_limits.ts
│ ├── set_msg_fee.ts
│ ├── solc_abigen.sh
│ ├── solt.sh
│ ├── transfer_ownership.ts
│ └── update_bridge_supply_cap.ts
├── test/
│ ├── Basics.spec.ts
│ ├── Bridge.spec.ts
│ ├── FarmingRewards.spec.ts
│ ├── Governance.spec.ts
│ ├── GovernedOwner.spec.ts
│ ├── Message.spec.ts
│ ├── MultiValidator.spec.ts
│ ├── Sentinel.spec.ts
│ ├── Slash.spec.ts
│ ├── StakingReward.spec.ts
│ ├── TransferSwap.spec.ts
│ ├── ValidatorSigner.spec.ts
│ └── lib/
│ ├── common.ts
│ ├── constants.ts
│ └── proto.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
# folders
artifacts/
bin/
build/
cache/
deployments/
genfiles/
node_modules/
typechain/
================================================
FILE: .eslintrc.js
================================================
module.exports = {
env: {
es2021: true,
mocha: true,
node: true
},
root: true,
extends: [
'prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/warnings'
],
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-use-before-define': 'error',
'arrow-body-style': 'off',
camelcase: ['error', { allow: ['^.*__factory$'] }],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never'
}
],
'import/no-extraneous-dependencies': 'off',
'import/no-useless-path-segments': 'off',
'import/prefer-default-export': 'off',
'no-console': 'off',
'no-empty-function': 'off',
'no-param-reassign': 'warn',
'no-plusplus': ['off'],
'no-underscore-dangle': 'warn',
'no-unused-vars': 'off',
'no-use-before-define': 'off',
'prefer-destructuring': 'off',
'prefer-template': 'off'
},
settings: {
'import/resolver': {
node: {
extensions: ['.d.ts', '.js', '.ts']
}
}
}
};
================================================
FILE: .github/workflows/abigen.yml
================================================
# run solc/abigen for go binding
name: gen-go-binding
on:
pull_request:
paths:
- '.github/workflows/abigen.yml'
- 'hardhat.config.ts'
- 'contracts/**'
- 'scripts/**'
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }} # so we get the topic branch to push to same PR, only available if triggered by pull request
- name: compile contracts and generate go binding
env:
PRID: ${{ github.event.number }}
BRANCH: ${{ github.head_ref }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
source scripts/solc_abigen.sh
setup_git
dld_solc && run_solc
dld_abigen && run_abigen
================================================
FILE: .github/workflows/pbsol.yml
================================================
# run pb gen sol if proto files change
name: pb-gen-sol
on:
pull_request:
paths:
- '.github/workflows/pbsol.yml'
- 'hardhat.config.ts'
- 'contracts/libraries/proto/**'
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }} # so we get the topic branch to push to same PR, only available if triggered by pull request
- name: run pb-gen-sol and push to same PR if Pbxxx.sol files are different
run: |
source scripts/pb_gen_sol.sh
prepare_tools && gen_sol && add_to_pr
================================================
FILE: .github/workflows/run_tests.yml
================================================
name: run-tests
on:
pull_request:
paths:
- '.github/workflows/run_tests.yml'
- 'hardhat.config.ts'
- 'contracts/**'
- 'test/**'
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22.3.0
- name: Installing dependencies
run: yarn install --frozen-lockfile
- name: Running tests
run: yarn test
================================================
FILE: .gitignore
================================================
artifacts/
artifacts-zk/
bin/
build/
cache/
cache-zk/
deployments/hardhat/
genfiles/
failing_metadata/
node_modules/
typechain/
*.pid
*.log
*.new
*.bak
.DS_Store
.vscode/
.idea/
.env
solc-input-*.json
flattened-*.sol
memo
================================================
FILE: .prettierrc.json
================================================
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
}
},
{
"files": ["*.js", "*.ts", "*.md"],
"options": {
"printWidth": 120
}
}
]
}
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
# SGN Contracts
Contracts for the Celer State Guardian Network (SGN) V2.
### Run unit tests
```sh
yarn test
```
### Benchmark gas cost
```sh
yarn report-gas:benchmark
yarn report-gas:summary
```
Check `reports/gas_usage`.
### Update contract sizes
```sh
yarn size-contracts
```
Check `reports/contract_sizes.txt`.
## Deployments
### Deployment Management
Contract deployments are tracked by hardhat through the files under ./deployments directory on deployment branches.
To deploy newest contracts to mainnet, staging, or testnet chains:
1. `git checkout mainnet-deployment|staging-deployment|testnet-deployment`, correspondingly
2. `git merge main` into the deployment branch
3. deploy the contracts
4. push the deployments file changes
If any contracts (e.g. libraries) are used for both mainnet and staging, follow the step above to deploy them on staging chains first, then cherry-pick the commit containing ONLY the deployment changes of these shared contracts to `mainnet-deployment`. Please be cautious with file changes when doing such operation.
Rules:
1. ./deployments should NOT exist on main branch
2. only merge main into the deployment branches
3. only change the ./deployments directory on deployment branches so that there will always be no conflicts when merge main
### Deploy contracts
1. `cp .env.template .env`, then ensure all environment variables are set in `.env`.
2. Replace `INFURA-PROJECT-ID` suffix of the network endpoint in `.env`, that you're going to use.
3. Add private key of your account that would be used, in `.env`. Refer to `hardhat.config.ts` for env param key.
4. Deploy SGN and Staking contracts:
```sh
hardhat deploy --network --tags SGNStaking
```
Deploy Bridge contract:
```sh
hardhat deploy --network --tags Bridge
```
Deploy OriginalTokenVault contract:
Make sure to set ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER in .env to the Bridge address when deploying.
Such as:
ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER=0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22
Where 0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22 is the Bridge contract address
```sh
hardhat deploy --network --tags OriginalTokenVault
```
Deploy PeggedTokenBridge contract:
Make sure to set ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER in .env to the Bridge address when deploying.
Such as:
PEGGED_TOKEN_BRIDGE_SIGS_VERIFIER=0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22
Where 0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22 is the Bridge contract address
```sh
hardhat deploy --network --tags PeggedTokenBridge
```
Deploy OriginalTokenVaultV2 contract:
Make sure to set ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER in .env to the Bridge address when deploying.
Such as:
ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER=0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22
Where 0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22 is the Bridge contract address
```sh
hardhat deploy --network --tags OriginalTokenVaultV2
```
Deploy PeggedTokenBridgeV2 contract:
Make sure to set ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER in .env to the Bridge address when deploying.
Such as:
PEGGED_TOKEN_BRIDGE_SIGS_VERIFIER=0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22
Where 0x67E5E3E54B2E4433CeDB484eCF4ef0f35Fe3Fb22 is the Bridge contract address
```sh
hardhat deploy --network --tags PeggedTokenBridgeV2
```
### Verify contracts on explorers
#### On Etherscan variants via hardhat etherscan-verify
This is the recommended way for most mainnet Etherscan variants.
Make sure the `ETHERSCAN_API_KEY` is set correctly in `.env`.
```sh
hardhat etherscan-verify --network --license "GPL-3.0" --force-license
```
#### On Etherscan variants via solt
This is useful since most testnet Etherscan variants don't offer verification via the API.
1. Generate the standard JSON input files:
```sh
source scripts/solt.sh
run_solt_write
```
2. Then try:
```sh
solt verify --license 5 --network solc-input-.json
```
3. If the second step fails, go to Etherscan and manually verify using the standard JSON input files.
#### On Blockscout variants via sourcify
This is used if the Blockscout variant requires "Sources and Metadata JSON".
```sh
hardhat sourcify --network
```
#### On Blockscout variants via flattened source files
This is used if the Blockscout variant requires a single source file, or in general as a last resort.
1. Flatten the source files:
```sh
hardhat flatten > flattened.sol
```
2. Edit `flattened.sol`. Remove the duplicate `SPDX-License-Identifier` lines, keeping a single copy of
```
// SPDX-License-Identifier: GPL-3.0-only
```
and submit to Blockscout.
Sometimes you also need to remove the duplicate `pragma solidity` lines.
## Upgradable contract via the proxy pattern
### How it works
proxy contract holds state and delegatecall all calls to actual impl contract. When upgrade, a new impl contract is deployed, and proxy is updated to point to the new contract. below from [openzeppelin doc](https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#upgrading-via-the-proxy-pattern)
```
User ---- tx ---> Proxy ----------> Implementation_v0
|
------------> Implementation_v1
|
------------> Implementation_v2
```
### Add upgradable contract
To minimize code fork, we add a new contract that inherits existing contract, eg. `contract TokenUpgradable is Token`. Next we need to ensure that all states set in Token contract constructor (and its parent contracts) must be settable via a separate normal func like `init`. This will allow Proxy contract to delegeteCall init and set proper values in Proxy's state, not the impl contract state. See MintSwapCanonicalTokenUpgradable.sol for example. We also need to either shadow Ownable._owner because when proxy delegateCall, in proxy state, Ownable._owner is not set and there is no other way to set it. Or use our own Ownable.sol which has internal func initOwner
### Add deploy scripts
add a new ts file for deploy, in deploy options, add proxy section, make sure the methodName and args match actual upgradable contract
```ts
proxy: {
proxyContract: "OptimizedTransparentProxy",
execute: {
// only called when proxy is deployed, it'll call Token contract.init
// with proper args
init: {
methodName: 'init',
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL]
}
}
}
```
see deploy/pegged/tokens/008_mint_swap_canonical_token_upgradable.ts for example
### Deploy and upgrade
hardhat deploy plugin tries to be smart and deploy ProxyAdmin only once for each chain, deploy impl contract then proxy contract
================================================
FILE: benchmark/relay.ts
================================================
import '@nomicfoundation/hardhat-ethers';
import { parseUnits, solidityPackedKeccak256, toNumber, Wallet } from 'ethers';
import fs from 'fs';
import { ethers } from 'hardhat';
import path from 'path';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { deployBridgeContracts, getAccounts } from '../test/lib/common';
import { getRelayRequest } from '../test/lib/proto';
import { Bridge, TestERC20 } from '../typechain';
const GAS_USAGE_DIR = 'reports/gas_usage/';
const GAS_USAGE_LOG = path.join(GAS_USAGE_DIR, 'relay.txt');
describe('Relay Gas Benchmark', function () {
if (!fs.existsSync(GAS_USAGE_DIR)) {
fs.mkdirSync(GAS_USAGE_DIR, { recursive: true });
}
fs.rmSync(GAS_USAGE_LOG, { force: true });
fs.appendFileSync(GAS_USAGE_LOG, ' for cbr testErc20 relay tx\n\n');
async function fixture() {
const [admin] = await ethers.getSigners();
const { bridge, token } = await deployBridgeContracts(admin);
return { admin, bridge, token };
}
let bridge: Bridge;
let token: TestERC20;
let admin: HardhatEthersSigner;
let accounts: Wallet[];
beforeEach(async () => {
const res = await loadFixture(fixture);
bridge = res.bridge;
token = res.token;
admin = res.admin;
accounts = await getAccounts(admin, [token], 21);
await token.transfer(bridge.getAddress(), parseUnits('1000000'));
await bridge.setEpochVolumeCaps([token.getAddress()], [parseUnits('100')]);
await bridge.setEpochLength(5);
await bridge.setDelayThresholds([token.getAddress()], [parseUnits('100')]);
});
it('benchmark relay gas cost for bridge', async function () {
let perSigCost;
for (let i = 5; i <= 21; i += 2) {
perSigCost = await doBenchmarkRelaySig(i);
}
fs.appendFileSync(GAS_USAGE_LOG, 'per sig cost: ' + perSigCost + '\n');
fs.appendFileSync(GAS_USAGE_LOG, '\n');
const perSignerCost = await doBenchmarkRelaySigner(21, 8);
fs.appendFileSync(GAS_USAGE_LOG, 'per validator cost: ' + perSignerCost + '\n');
});
async function getPowers(
accounts: Wallet[],
signerNum: number,
quorumSigNum: number
): Promise<{ signers: Wallet[]; addrs: string[]; powers: bigint[] }> {
const signers: Wallet[] = [];
const addrs: string[] = [];
const powers: bigint[] = [];
for (let i = 0; i < signerNum; i++) {
signers.push(accounts[i]);
addrs.push(accounts[i].address);
if (i == quorumSigNum - 1) {
powers.push(parseUnits('100'));
} else {
powers.push(parseUnits('1'));
}
}
return { signers, addrs, powers };
}
async function doBenchmarkRelaySig(signerNum: number) {
let firstCost = 0;
let lastCost = 0;
const maxQuorumSigNum = ((signerNum * 2) / 3 + 1) | 0;
for (let i = 3; i <= maxQuorumSigNum; i += 2) {
const gasUsed = await doBenchmarkRelay(signerNum, i);
if (i == 3) {
firstCost = toNumber(gasUsed);
}
lastCost = toNumber(gasUsed);
}
const perSigCost = Math.ceil((lastCost - firstCost) / (maxQuorumSigNum - 3));
return perSigCost;
}
async function doBenchmarkRelaySigner(maxSignerNum: number, quorumSigNum: number) {
let firstCost = 0;
let lastCost = 0;
const minSignerNum = ((quorumSigNum * 3) / 2) | 0;
for (let i = minSignerNum; i <= maxSignerNum; i++) {
const gasUsed = await doBenchmarkRelay(i, quorumSigNum);
if (i == minSignerNum) {
firstCost = toNumber(gasUsed);
}
lastCost = toNumber(gasUsed);
}
const perSignerCost = Math.ceil((lastCost - firstCost) / (maxSignerNum - minSignerNum));
return perSignerCost;
}
async function doBenchmarkRelay(signerNum: number, quorumSigNum: number) {
if (quorumSigNum > signerNum) {
quorumSigNum = signerNum;
}
const { signers, addrs, powers } = await getPowers(accounts, signerNum, quorumSigNum);
await bridge.notifyResetSigners();
await bridge.resetSigners(addrs, powers);
const sender = accounts[0];
const receiver = accounts[1];
const amount = parseUnits('1');
const chainId = toNumber((await ethers.provider.getNetwork()).chainId);
const srcXferId = solidityPackedKeccak256(['uint64'], [Date.now()]); // fake src xfer id
const { relayBytes, sigs } = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
amount,
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
const gasUsed = (await (await bridge.relay(relayBytes, sigs, addrs, powers)).wait())!.gasUsed;
fs.appendFileSync(GAS_USAGE_LOG, signerNum.toString() + '\t' + quorumSigNum.toString() + '\t' + gasUsed + '\n');
return gasUsed;
}
});
================================================
FILE: contracts/circle-usdc/CircleBridgeProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./FeeOperator.sol";
import "../interfaces/ICircleBridge.sol";
import "../safeguard/Governor.sol";
import "../safeguard/Pauser.sol";
contract CircleBridgeProxy is FeeOperator, Governor, Pauser, ReentrancyGuard {
using SafeERC20 for IERC20;
address public immutable circleBridge;
uint32 public feePercGlobal; //in 1e6
// chainId => feePercOverride, support override fee perc by dst chain
mapping(uint64 => uint32) public feePercOverride;
/// per dest chain id executor fee in this chain's USDC token
mapping(uint64 => uint256) public dstTxFee;
// 0 is regarded as not registered. Set to a negative value if target domain is actually 0.
mapping(uint64 => int32) public chidToDomain;
event FeePercUpdated(uint64[] chainIds, uint32[] feePercs);
event TxFeeUpdated(uint64[] chainIds, uint256[] fees);
event ChidToDomainUpdated(uint64[] chainIds, int32[] domains);
event Deposited(
address sender,
bytes32 recipient,
uint64 dstChid,
uint256 amount,
uint256 txFee,
uint256 percFee,
uint64 nonce
);
constructor(address _circleBridge, address _feeCollector) FeeOperator(_feeCollector) {
circleBridge = _circleBridge;
}
function depositForBurn(
uint256 _amount,
uint64 _dstChid,
bytes32 _mintRecipient,
address _burnToken
) external nonReentrant whenNotPaused returns (uint64 _nonce) {
int32 dstDomain = chidToDomain[_dstChid];
require(dstDomain != 0, "dst domain not registered");
if (dstDomain < 0) {
dstDomain = 0; // a negative value indicates the target domain is 0 actually.
}
(uint256 fee, uint256 txFee, uint256 percFee) = totalFee(_amount, _dstChid);
require(_amount > fee, "fee not covered");
IERC20(_burnToken).safeTransferFrom(msg.sender, address(this), _amount);
uint256 bridgeAmt = _amount - fee;
IERC20(_burnToken).safeIncreaseAllowance(circleBridge, bridgeAmt);
_nonce = ICircleBridge(circleBridge).depositForBurn(bridgeAmt, uint32(dstDomain), _mintRecipient, _burnToken);
IERC20(_burnToken).safeApprove(circleBridge, 0);
emit Deposited(msg.sender, _mintRecipient, _dstChid, _amount, txFee, percFee, _nonce);
}
function totalFee(uint256 _amount, uint64 _dstChid)
public
view
returns (
uint256 _fee,
uint256 _txFee,
uint256 _percFee
)
{
uint32 feePerc = feePercOverride[_dstChid];
if (feePerc == 0) {
feePerc = feePercGlobal;
}
_txFee = dstTxFee[_dstChid];
_percFee = (_amount * feePerc) / 1e6;
_fee = _txFee + _percFee;
}
function setFeePerc(uint64[] calldata _chainIds, uint32[] calldata _feePercs) external onlyGovernor {
require(_chainIds.length == _feePercs.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
require(_feePercs[i] < 1e6, "fee percentage too large");
if (_chainIds[i] == 0) {
feePercGlobal = _feePercs[i];
} else {
feePercOverride[_chainIds[i]] = _feePercs[i];
}
}
emit FeePercUpdated(_chainIds, _feePercs);
}
function setTxFee(uint64[] calldata _chainIds, uint256[] calldata _fees) external onlyGovernor {
require(_chainIds.length == _fees.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
dstTxFee[_chainIds[i]] = _fees[i];
}
emit TxFeeUpdated(_chainIds, _fees);
}
function setChidToDomain(uint64[] calldata _chainIds, int32[] calldata _domains) external onlyGovernor {
require(_chainIds.length == _domains.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
chidToDomain[_chainIds[i]] = _domains[i];
}
emit ChidToDomainUpdated(_chainIds, _domains);
}
}
================================================
FILE: contracts/circle-usdc/CircleBridgeProxyV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./FeeOperator.sol";
import "../interfaces/ICircleBridge.sol";
import "../safeguard/Governor.sol";
import "../safeguard/Pauser.sol";
contract CircleBridgeProxyV2 is FeeOperator, Governor, Pauser, ReentrancyGuard {
using SafeERC20 for IERC20;
address public immutable circleBridge;
uint32 public feePercGlobal; //in 1e6
// chainId => feePercOverride, support override fee perc by dst chain
mapping(uint64 => uint32) public feePercOverride;
/// per dest chain id executor fee in this chain's USDC token
mapping(uint64 => uint256) public dstTxFee;
// 0 is regarded as not registered. Set to a negative value if target domain is actually 0.
mapping(uint64 => int32) public chidToDomain;
event FeePercUpdated(uint64[] chainIds, uint32[] feePercs);
event TxFeeUpdated(uint64[] chainIds, uint256[] fees);
event ChidToDomainUpdated(uint64[] chainIds, int32[] domains);
event Deposited(
address sender,
bytes32 recipient,
uint64 dstChid,
uint256 amount,
uint256 txFee,
uint256 percFee,
uint32 minFinalityThreshold
);
constructor(address _circleBridge, address _feeCollector) FeeOperator(_feeCollector) {
circleBridge = _circleBridge;
}
function depositForBurn(
uint256 _amount,
uint64 _dstChid,
bytes32 _mintRecipient,
address _burnToken,
uint256 _maxFee,
uint32 _minFinalityThreshold
) external nonReentrant whenNotPaused {
int32 dstDomain = chidToDomain[_dstChid];
require(dstDomain != 0, "dst domain not registered");
if (dstDomain < 0) {
dstDomain = 0; // a negative value indicates the target domain is 0 actually.
}
(uint256 fee, uint256 txFee, uint256 percFee) = totalFee(_amount, _dstChid);
require(_amount > fee, "fee not covered");
IERC20(_burnToken).safeTransferFrom(msg.sender, address(this), _amount);
uint256 bridgeAmt = _amount - fee;
IERC20(_burnToken).safeIncreaseAllowance(circleBridge, bridgeAmt);
ICircleBridge(circleBridge).depositForBurn(bridgeAmt, uint32(dstDomain), _mintRecipient, _burnToken, bytes32(0), _maxFee, _minFinalityThreshold);
IERC20(_burnToken).safeApprove(circleBridge, 0);
emit Deposited(msg.sender, _mintRecipient, _dstChid, _amount, txFee, percFee, _minFinalityThreshold);
}
function totalFee(uint256 _amount, uint64 _dstChid)
public
view
returns (
uint256 _fee,
uint256 _txFee,
uint256 _percFee
)
{
uint32 feePerc = feePercOverride[_dstChid];
if (feePerc == 0) {
feePerc = feePercGlobal;
}
_txFee = dstTxFee[_dstChid];
_percFee = (_amount * feePerc) / 1e6;
_fee = _txFee + _percFee;
}
function setFeePerc(uint64[] calldata _chainIds, uint32[] calldata _feePercs) external onlyGovernor {
require(_chainIds.length == _feePercs.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
require(_feePercs[i] < 1e6, "fee percentage too large");
if (_chainIds[i] == 0) {
feePercGlobal = _feePercs[i];
} else {
feePercOverride[_chainIds[i]] = _feePercs[i];
}
}
emit FeePercUpdated(_chainIds, _feePercs);
}
function setTxFee(uint64[] calldata _chainIds, uint256[] calldata _fees) external onlyGovernor {
require(_chainIds.length == _fees.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
dstTxFee[_chainIds[i]] = _fees[i];
}
emit TxFeeUpdated(_chainIds, _fees);
}
function setChidToDomain(uint64[] calldata _chainIds, int32[] calldata _domains) external onlyGovernor {
require(_chainIds.length == _domains.length, "length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
chidToDomain[_chainIds[i]] = _domains[i];
}
emit ChidToDomainUpdated(_chainIds, _domains);
}
}
================================================
FILE: contracts/circle-usdc/FeeOperator.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../safeguard/Ownable.sol";
abstract contract FeeOperator is Ownable {
using SafeERC20 for IERC20;
address public feeCollector;
event FeeCollectorUpdated(address from, address to);
modifier onlyFeeCollector() {
require(msg.sender == feeCollector, "not fee collector");
_;
}
constructor(address _feeCollector) {
feeCollector = _feeCollector;
}
function collectFee(address[] calldata _tokens, address _to) external onlyFeeCollector {
for (uint256 i = 0; i < _tokens.length; i++) {
// use zero address to denote native token
if (_tokens[i] == address(0)) {
uint256 bal = address(this).balance;
(bool sent, ) = _to.call{value: bal, gas: 50000}("");
require(sent, "send native failed");
} else {
uint256 balance = IERC20(_tokens[i]).balanceOf(address(this));
IERC20(_tokens[i]).safeTransfer(_to, balance);
}
}
}
function setFeeCollector(address _feeCollector) external onlyOwner {
address oldFeeCollector = feeCollector;
feeCollector = _feeCollector;
emit FeeCollectorUpdated(oldFeeCollector, _feeCollector);
}
}
================================================
FILE: contracts/governed-owner/GovernedOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./proxies/CommonOwnerProxy.sol";
import "./proxies/BridgeOwnerProxy.sol";
import "./proxies/MessageOwnerProxy.sol";
import "./proxies/SgnOwnerProxy.sol";
import "./proxies/UpgradeableOwnerProxy.sol";
contract GovernedOwnerProxy is
CommonOwnerProxy,
BridgeOwnerProxy,
MessageOwnerProxy,
SgnOwnerProxy,
UpgradeableOwnerProxy
{
constructor(address _initializer) OwnerProxyBase(_initializer) {}
}
================================================
FILE: contracts/governed-owner/SimpleGovernance.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/Utils.sol";
// mainly used for governed multi owner to do infrequent owner operations,
// relatively prefer easy-to-use over gas-efficiency
contract SimpleGovernance {
uint256 public constant THRESHOLD_DECIMAL = 100;
uint256 public constant MIN_ACTIVE_PERIOD = 3600; // one hour
uint256 public constant MAX_ACTIVE_PERIOD = 2419200; // four weeks
using SafeERC20 for IERC20;
enum ParamName {
ActivePeriod,
QuorumThreshold, // default threshold for votes to pass
FastPassThreshold // lower threshold for less critical operations
}
enum ProposalType {
ExternalDefault,
ExternalFastPass,
InternalParamChange,
InternalVoterUpdate,
InternalProxyUpdate,
InternalTransferToken
}
mapping(ParamName => uint256) public params;
struct Proposal {
bytes32 dataHash; // hash(proposalType, targetAddress, calldata)
uint256 deadline;
mapping(address => bool) votes;
}
mapping(uint256 => Proposal) public proposals;
uint256 public nextProposalId;
address[] public voters;
mapping(address => uint256) public voterPowers; // voter addr -> voting power
// NOTE: proxies must be audited open-source non-upgradable contracts with following requirements:
// 1. Truthfully pass along tx sender who called the proxy function as the governance proposer.
// 2. Do not allow arbitrary fastpass proposal with calldata constructed by the proxy callers.
// See ./proxies/CommonOwnerProxy.sol for example.
mapping(address => bool) public proposerProxies;
uint256 public nativeTokenTransferGas = 50000;
event Initiated(
address[] voters,
uint256[] powers,
address[] proxies,
uint256 activePeriod,
uint256 quorumThreshold,
uint256 fastPassThreshold
);
event ProposalCreated(
uint256 proposalId,
ProposalType proposalType,
address target,
bytes data,
uint256 deadline,
address proposer
);
event ProposalVoted(uint256 proposalId, address voter, bool vote);
event ProposalExecuted(uint256 proposalId);
event ParamChangeProposalCreated(uint256 proposalId, ParamName name, uint256 value);
event VoterUpdateProposalCreated(uint256 proposalId, address[] voters, uint256[] powers);
event ProxyUpdateProposalCreated(uint256 proposalId, address[] addrs, bool[] ops);
event TransferTokenProposalCreated(uint256 proposalId, address receiver, address token, uint256 amount);
constructor(
address[] memory _voters,
uint256[] memory _powers,
address[] memory _proxies,
uint256 _activePeriod,
uint256 _quorumThreshold,
uint256 _fastPassThreshold
) {
require(_voters.length > 0 && _voters.length == _powers.length, "invalid init voters");
require(_activePeriod <= MAX_ACTIVE_PERIOD && _activePeriod >= MIN_ACTIVE_PERIOD, "invalid active period");
require(
_quorumThreshold < THRESHOLD_DECIMAL && _fastPassThreshold <= _quorumThreshold,
"invalid init thresholds"
);
for (uint256 i = 0; i < _voters.length; i++) {
_setVoter(_voters[i], _powers[i]);
}
for (uint256 i = 0; i < _proxies.length; i++) {
proposerProxies[_proxies[i]] = true;
}
params[ParamName.ActivePeriod] = _activePeriod;
params[ParamName.QuorumThreshold] = _quorumThreshold;
params[ParamName.FastPassThreshold] = _fastPassThreshold;
emit Initiated(_voters, _powers, _proxies, _activePeriod, _quorumThreshold, _fastPassThreshold);
}
/*********************************
* External and Public Functions *
*********************************/
function createProposal(address _target, bytes memory _data) external returns (uint256) {
return _createProposal(msg.sender, _target, _data, ProposalType.ExternalDefault);
}
// create proposal through proxy
function createProposal(
address _proposer,
address _target,
bytes memory _data,
ProposalType _type
) external returns (uint256) {
require(proposerProxies[msg.sender], "sender is not a valid proxy");
require(_type == ProposalType.ExternalDefault || _type == ProposalType.ExternalFastPass, "invalid type");
return _createProposal(_proposer, _target, _data, _type);
}
function createParamChangeProposal(ParamName _name, uint256 _value) external returns (uint256) {
bytes memory data = abi.encode(_name, _value);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalParamChange);
emit ParamChangeProposalCreated(proposalId, _name, _value);
return proposalId;
}
function createVoterUpdateProposal(address[] calldata _voters, uint256[] calldata _powers)
external
returns (uint256)
{
require(_voters.length == _powers.length, "voters and powers length not match");
bytes memory data = abi.encode(_voters, _powers);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalVoterUpdate);
emit VoterUpdateProposalCreated(proposalId, _voters, _powers);
return proposalId;
}
function createProxyUpdateProposal(address[] calldata _addrs, bool[] calldata _ops) external returns (uint256) {
require(_addrs.length == _ops.length, "_addrs and _ops length not match");
bytes memory data = abi.encode(_addrs, _ops);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalProxyUpdate);
emit ProxyUpdateProposalCreated(proposalId, _addrs, _ops);
return proposalId;
}
function createTransferTokenProposal(
address _receiver,
address _token,
uint256 _amount
) external returns (uint256) {
bytes memory data = abi.encode(_receiver, _token, _amount);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalTransferToken);
emit TransferTokenProposalCreated(proposalId, _receiver, _token, _amount);
return proposalId;
}
function voteProposal(uint256 _proposalId, bool _vote) public {
require(voterPowers[msg.sender] > 0, "invalid voter");
Proposal storage p = proposals[_proposalId];
require(block.timestamp < p.deadline, "deadline passed");
p.votes[msg.sender] = _vote;
emit ProposalVoted(_proposalId, msg.sender, _vote);
}
function voteProposals(uint256[] calldata _proposalIds, bool[] calldata _votes) external {
require(_proposalIds.length == _votes.length, "proposalIds and votes length not match");
for (uint256 i = 0; i < _proposalIds.length; i++) {
voteProposal(_proposalIds[i], _votes[i]);
}
}
function executeProposal(
uint256 _proposalId,
ProposalType _type,
address _target,
bytes calldata _data
) external {
require(voterPowers[msg.sender] > 0, "only voter can execute a proposal");
Proposal storage p = proposals[_proposalId];
require(block.timestamp < p.deadline, "deadline passed");
require(keccak256(abi.encodePacked(_type, _target, _data)) == p.dataHash, "data hash not match");
p.deadline = 0;
p.votes[msg.sender] = true;
(, , bool pass) = countVotes(_proposalId, _type);
require(pass, "not enough votes");
if (_type == ProposalType.ExternalDefault || _type == ProposalType.ExternalFastPass) {
(bool success, bytes memory res) = _target.call(_data);
require(success, Utils.getRevertMsg(res));
} else if (_type == ProposalType.InternalParamChange) {
(ParamName name, uint256 value) = abi.decode((_data), (ParamName, uint256));
params[name] = value;
if (name == ParamName.ActivePeriod) {
require(value <= MAX_ACTIVE_PERIOD && value >= MIN_ACTIVE_PERIOD, "invalid active period");
} else if (name == ParamName.QuorumThreshold || name == ParamName.FastPassThreshold) {
require(
params[ParamName.QuorumThreshold] >= params[ParamName.FastPassThreshold] &&
value < THRESHOLD_DECIMAL &&
value > 0,
"invalid threshold"
);
}
} else if (_type == ProposalType.InternalVoterUpdate) {
(address[] memory addrs, uint256[] memory powers) = abi.decode((_data), (address[], uint256[]));
for (uint256 i = 0; i < addrs.length; i++) {
if (powers[i] > 0) {
_setVoter(addrs[i], powers[i]);
} else {
_removeVoter(addrs[i]);
}
}
} else if (_type == ProposalType.InternalProxyUpdate) {
(address[] memory addrs, bool[] memory ops) = abi.decode((_data), (address[], bool[]));
for (uint256 i = 0; i < addrs.length; i++) {
if (ops[i]) {
proposerProxies[addrs[i]] = true;
} else {
delete proposerProxies[addrs[i]];
}
}
} else if (_type == ProposalType.InternalTransferToken) {
(address receiver, address token, uint256 amount) = abi.decode((_data), (address, address, uint256));
_transfer(receiver, token, amount);
}
emit ProposalExecuted(_proposalId);
}
function setNativeTokenTransferGas(uint256 _gasUsed) external {
require(voterPowers[msg.sender] > 0, "invalid caller");
nativeTokenTransferGas = _gasUsed;
}
receive() external payable {}
/**************************
* Public View Functions *
**************************/
function getVoters() public view returns (address[] memory, uint256[] memory) {
address[] memory addrs = new address[](voters.length);
uint256[] memory powers = new uint256[](voters.length);
for (uint32 i = 0; i < voters.length; i++) {
addrs[i] = voters[i];
powers[i] = voterPowers[voters[i]];
}
return (addrs, powers);
}
function getVote(uint256 _proposalId, address _voter) public view returns (bool) {
return proposals[_proposalId].votes[_voter];
}
function countVotes(uint256 _proposalId, ProposalType _type)
public
view
returns (
uint256,
uint256,
bool
)
{
uint256 yesVotes;
uint256 totalPower;
for (uint32 i = 0; i < voters.length; i++) {
if (getVote(_proposalId, voters[i])) {
yesVotes += voterPowers[voters[i]];
}
totalPower += voterPowers[voters[i]];
}
uint256 threshold;
if (_type == ProposalType.ExternalFastPass) {
threshold = params[ParamName.FastPassThreshold];
} else {
threshold = params[ParamName.QuorumThreshold];
}
bool pass = (yesVotes >= (totalPower * threshold) / THRESHOLD_DECIMAL);
return (totalPower, yesVotes, pass);
}
/**********************************
* Internal and Private Functions *
**********************************/
// create a proposal and vote yes
function _createProposal(
address _proposer,
address _target,
bytes memory _data,
ProposalType _type
) private returns (uint256) {
require(voterPowers[_proposer] > 0, "only voter can create a proposal");
uint256 proposalId = nextProposalId;
nextProposalId += 1;
Proposal storage p = proposals[proposalId];
p.dataHash = keccak256(abi.encodePacked(_type, _target, _data));
p.deadline = block.timestamp + params[ParamName.ActivePeriod];
p.votes[_proposer] = true;
emit ProposalCreated(proposalId, _type, _target, _data, p.deadline, _proposer);
return proposalId;
}
function _setVoter(address _voter, uint256 _power) private {
require(_power > 0, "zero power");
if (voterPowers[_voter] == 0) {
// add new voter
voters.push(_voter);
}
voterPowers[_voter] = _power;
}
function _removeVoter(address _voter) private {
require(voterPowers[_voter] > 0, "not a voter");
uint256 lastIndex = voters.length - 1;
for (uint256 i = 0; i < voters.length; i++) {
if (voters[i] == _voter) {
if (i < lastIndex) {
voters[i] = voters[lastIndex];
}
voters.pop();
voterPowers[_voter] = 0;
return;
}
}
revert("voter not found"); // this should never happen
}
function _transfer(
address _receiver,
address _token,
uint256 _amount
) private {
if (_token == address(0)) {
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
}
================================================
FILE: contracts/governed-owner/customized/MessageBusOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../../libraries/Utils.sol";
import "../interfaces/IMessageOwner.sol";
// only allow set MsgFee and PreExecuteMessageGasUsage
// disable contract upgrade or token bridge address updates
contract MessageBusOwner {
uint256 public constant THRESHOLD_DECIMAL = 100;
uint256 public constant MIN_ACTIVE_PERIOD = 3600; // one hour
uint256 public constant MAX_ACTIVE_PERIOD = 2419200; // four weeks
enum ParamName {
ActivePeriod,
QuorumThreshold // threshold for votes to pass
}
enum ProposalType {
External,
InternalParamChange,
InternalVoterUpdate
}
enum MsgFeeType {
PerByte,
Base
}
mapping(ParamName => uint256) public params;
struct Proposal {
bytes32 dataHash; // hash(proposalType, targetAddress, calldata)
uint256 deadline;
mapping(address => bool) votes;
}
mapping(uint256 => Proposal) public proposals;
uint256 public nextProposalId;
address[] public voters;
mapping(address => uint256) public voterPowers; // voter addr -> voting power
event Initiated(address[] voters, uint256[] powers, uint256 activePeriod, uint256 quorumThreshold);
event ProposalCreated(
uint256 proposalId,
ProposalType proposalType,
address target,
bytes data,
uint256 deadline,
address proposer
);
event ParamChangeProposalCreated(uint256 proposalId, ParamName name, uint256 value);
event VoterUpdateProposalCreated(uint256 proposalId, address[] voters, uint256[] powers);
event SetMsgFeeProposalCreated(uint256 proposalId, address target, MsgFeeType feeType, uint256 fee);
event SetPreExecuteMessageGasUsageProposalCreated(uint256 proposalId, address target, uint256 usage);
event ProposalVoted(uint256 proposalId, address voter, bool vote);
event ProposalExecuted(uint256 proposalId);
constructor(
address[] memory _voters,
uint256[] memory _powers,
uint256 _activePeriod,
uint256 _quorumThreshold
) {
require(_voters.length > 0 && _voters.length == _powers.length, "invalid init voters");
require(_activePeriod <= MAX_ACTIVE_PERIOD && _activePeriod >= MIN_ACTIVE_PERIOD, "invalid active period");
require(_quorumThreshold < THRESHOLD_DECIMAL, "invalid init thresholds");
for (uint256 i = 0; i < _voters.length; i++) {
_setVoter(_voters[i], _powers[i]);
}
params[ParamName.ActivePeriod] = _activePeriod;
params[ParamName.QuorumThreshold] = _quorumThreshold;
emit Initiated(_voters, _powers, _activePeriod, _quorumThreshold);
}
/*********************************
* External and Public Functions *
*********************************/
function proposeParamChange(ParamName _name, uint256 _value) external returns (uint256) {
bytes memory data = abi.encode(_name, _value);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalParamChange);
emit ParamChangeProposalCreated(proposalId, _name, _value);
return proposalId;
}
function proposeVoterUpdate(address[] calldata _voters, uint256[] calldata _powers) external returns (uint256) {
require(_voters.length == _powers.length, "voters and powers length not match");
bytes memory data = abi.encode(_voters, _powers);
uint256 proposalId = _createProposal(msg.sender, address(0), data, ProposalType.InternalVoterUpdate);
emit VoterUpdateProposalCreated(proposalId, _voters, _powers);
return proposalId;
}
function proposeSetMsgFee(
address _target,
MsgFeeType _feeType,
uint256 _fee
) external returns (uint256) {
bytes4 selector;
if (_feeType == MsgFeeType.PerByte) {
selector = IMessageOwner.setFeePerByte.selector;
} else if (_feeType == MsgFeeType.Base) {
selector = IMessageOwner.setFeeBase.selector;
} else {
revert("invalid fee type");
}
bytes memory data = abi.encodeWithSelector(selector, _fee);
uint256 proposalId = _createProposal(msg.sender, _target, data, ProposalType.External);
emit SetMsgFeeProposalCreated(proposalId, _target, _feeType, _fee);
return proposalId;
}
function proposeSetPreExecuteMessageGasUsage(address _target, uint256 _usage) external {
bytes memory data = abi.encodeWithSelector(IMessageOwner.setPreExecuteMessageGasUsage.selector, _usage);
uint256 proposalId = _createProposal(msg.sender, _target, data, ProposalType.External);
emit SetPreExecuteMessageGasUsageProposalCreated(proposalId, _target, _usage);
}
function voteProposal(uint256 _proposalId, bool _vote) external {
require(voterPowers[msg.sender] > 0, "invalid voter");
Proposal storage p = proposals[_proposalId];
require(block.timestamp < p.deadline, "deadline passed");
p.votes[msg.sender] = _vote;
emit ProposalVoted(_proposalId, msg.sender, _vote);
}
function executeProposal(
uint256 _proposalId,
ProposalType _type,
address _target,
bytes calldata _data
) external {
require(voterPowers[msg.sender] > 0, "only voter can execute a proposal");
Proposal storage p = proposals[_proposalId];
require(block.timestamp < p.deadline, "deadline passed");
require(keccak256(abi.encodePacked(_type, _target, _data)) == p.dataHash, "data hash not match");
p.deadline = 0;
p.votes[msg.sender] = true;
(, , bool pass) = countVotes(_proposalId);
require(pass, "not enough votes");
if (_type == ProposalType.External) {
(bool success, bytes memory res) = _target.call(_data);
require(success, Utils.getRevertMsg(res));
} else if (_type == ProposalType.InternalParamChange) {
(ParamName name, uint256 value) = abi.decode((_data), (ParamName, uint256));
params[name] = value;
if (name == ParamName.ActivePeriod) {
require(value <= MAX_ACTIVE_PERIOD && value >= MIN_ACTIVE_PERIOD, "invalid active period");
} else if (name == ParamName.QuorumThreshold) {
require(value < THRESHOLD_DECIMAL && value > 0, "invalid threshold");
}
} else if (_type == ProposalType.InternalVoterUpdate) {
(address[] memory addrs, uint256[] memory powers) = abi.decode((_data), (address[], uint256[]));
for (uint256 i = 0; i < addrs.length; i++) {
if (powers[i] > 0) {
_setVoter(addrs[i], powers[i]);
} else {
_removeVoter(addrs[i]);
}
}
}
emit ProposalExecuted(_proposalId);
}
/**************************
* Public View Functions *
**************************/
function getVoters() public view returns (address[] memory, uint256[] memory) {
address[] memory addrs = new address[](voters.length);
uint256[] memory powers = new uint256[](voters.length);
for (uint32 i = 0; i < voters.length; i++) {
addrs[i] = voters[i];
powers[i] = voterPowers[voters[i]];
}
return (addrs, powers);
}
function getVote(uint256 _proposalId, address _voter) public view returns (bool) {
return proposals[_proposalId].votes[_voter];
}
function countVotes(uint256 _proposalId)
public
view
returns (
uint256,
uint256,
bool
)
{
uint256 yesVotes;
uint256 totalPower;
for (uint32 i = 0; i < voters.length; i++) {
if (getVote(_proposalId, voters[i])) {
yesVotes += voterPowers[voters[i]];
}
totalPower += voterPowers[voters[i]];
}
uint256 threshold = params[ParamName.QuorumThreshold];
bool pass = (yesVotes >= (totalPower * threshold) / THRESHOLD_DECIMAL);
return (totalPower, yesVotes, pass);
}
/**********************************
* Internal and Private Functions *
**********************************/
// create a proposal and vote yes
function _createProposal(
address _proposer,
address _target,
bytes memory _data,
ProposalType _type
) private returns (uint256) {
require(voterPowers[_proposer] > 0, "only voter can create a proposal");
uint256 proposalId = nextProposalId;
nextProposalId += 1;
Proposal storage p = proposals[proposalId];
p.dataHash = keccak256(abi.encodePacked(_type, _target, _data));
p.deadline = block.timestamp + params[ParamName.ActivePeriod];
p.votes[_proposer] = true;
emit ProposalCreated(proposalId, _type, _target, _data, p.deadline, _proposer);
return proposalId;
}
function _setVoter(address _voter, uint256 _power) private {
require(_power > 0, "zero power");
if (voterPowers[_voter] == 0) {
// add new voter
voters.push(_voter);
}
voterPowers[_voter] = _power;
}
function _removeVoter(address _voter) private {
require(voterPowers[_voter] > 0, "not a voter");
uint256 lastIndex = voters.length - 1;
for (uint256 i = 0; i < voters.length; i++) {
if (voters[i] == _voter) {
if (i < lastIndex) {
voters[i] = voters[lastIndex];
}
voters.pop();
voterPowers[_voter] = 0;
return;
}
}
revert("voter not found"); // this should never happen
}
}
================================================
FILE: contracts/governed-owner/interfaces/IBridgeOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IBridgeOwner {
// for bridges
function resetSigners(address[] calldata _signers, uint256[] calldata _powers) external;
function notifyResetSigners() external;
function increaseNoticePeriod(uint256 _period) external;
function setWrap(address _token) external;
function setSupply(address _token, uint256 _supply) external;
function increaseSupply(address _token, uint256 _delta) external;
function decreaseSupply(address _token, uint256 _delta) external;
function addGovernor(address _account) external;
function removeGovernor(address _account) external;
// for bridge tokens
function updateBridge(address _bridge) external;
function updateBridgeSupplyCap(address _bridge, uint256 _cap) external;
function setBridgeTokenSwapCap(address _bridgeToken, uint256 _swapCap) external;
}
================================================
FILE: contracts/governed-owner/interfaces/ICommonOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface ICommonOwner {
function transferOwnership(address _newOwner) external;
function addPauser(address _account) external;
function removePauser(address _account) external;
}
================================================
FILE: contracts/governed-owner/interfaces/IMessageOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IMessageOwner {
function setFeePerByte(uint256 _fee) external;
function setFeeBase(uint256 _fee) external;
function setLiquidityBridge(address _addr) external;
function setPegBridge(address _addr) external;
function setPegVault(address _addr) external;
function setPegBridgeV2(address _addr) external;
function setPegVaultV2(address _addr) external;
function setPreExecuteMessageGasUsage(uint256 _usage) external;
}
================================================
FILE: contracts/governed-owner/interfaces/ISgnOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface ISgnOwner {
function setWhitelistEnabled(bool _whitelistEnabled) external;
function addWhitelisted(address _account) external;
function removeWhitelisted(address _account) external;
function setGovContract(address _addr) external;
function setRewardContract(address _addr) external;
function setMaxSlashFactor(uint256 _maxSlashFactor) external;
}
================================================
FILE: contracts/governed-owner/interfaces/IUpgradeableOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IUpgradeableOwner {
function changeProxyAdmin(address _proxy, address _newAdmin) external;
function upgrade(address _proxy, address _implementation) external;
function upgradeAndCall(
address _proxy,
address _implementation,
bytes calldata _data
) external;
function upgradeTo(address _implementation) external;
function upgradeToAndCall(address _implementation, bytes calldata _data) external;
}
================================================
FILE: contracts/governed-owner/proxies/BridgeOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./OwnerProxyBase.sol";
import "../interfaces/IBridgeOwner.sol";
import {SimpleGovernance as sg} from "../SimpleGovernance.sol";
import {OwnerDataTypes as dt} from "./OwnerDataTypes.sol";
abstract contract BridgeOwnerProxy is OwnerProxyBase {
// for bridges
event ResetSignersProposalCreated(uint256 proposalId, address target, address[] signers, uint256[] powers);
event NotifyResetSignersProposalCreated(uint256 proposalId, address target);
event IncreaseNoticePeriodProposalCreated(uint256 proposalId, address target, uint256 period);
event SetNativeWrapProposalCreated(uint256 proposalId, address target, address token);
event UpdateSupplyProposalCreated(
uint256 proposalId,
address target,
dt.Action action,
address token,
uint256 supply
);
event UpdateGovernorProposalCreated(uint256 proposalId, address target, dt.Action action, address account);
// for bridge tokens
event UpdateBridgeProposalCreated(uint256 proposalId, address target, address bridgeAddr);
event UpdateBridgeSupplyCapProposalCreated(uint256 proposalId, address target, address bridge, uint256 cap);
event SetBridgeTokenSwapCapProposalCreated(uint256 proposalId, address target, address bridgeToken, uint256 cap);
function proposeResetSigners(
address _target,
address[] calldata _signers,
uint256[] calldata _powers
) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.resetSigners.selector, _signers, _powers);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit ResetSignersProposalCreated(proposalId, _target, _signers, _powers);
}
function proposeNotifyResetSigners(address _target) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.notifyResetSigners.selector);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit NotifyResetSignersProposalCreated(proposalId, _target);
}
function proposeIncreaseNoticePeriod(address _target, uint256 _period) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.increaseNoticePeriod.selector, _period);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit IncreaseNoticePeriodProposalCreated(proposalId, _target, _period);
}
function proposeSetNativeWrap(address _target, address _token) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.setWrap.selector, _token);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetNativeWrapProposalCreated(proposalId, _target, _token);
}
function proposeUpdateSupply(
address _target,
dt.Action _action,
address _token,
uint256 _supply
) external {
bytes4 selector;
if (_action == dt.Action.Set) {
selector = IBridgeOwner.setSupply.selector;
} else if (_action == dt.Action.Add) {
selector = IBridgeOwner.increaseSupply.selector;
} else if (_action == dt.Action.Remove) {
selector = IBridgeOwner.decreaseSupply.selector;
} else {
revert("invalid action");
}
bytes memory data = abi.encodeWithSelector(selector, _token, _supply);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit UpdateSupplyProposalCreated(proposalId, _target, _action, _token, _supply);
}
function proposeUpdateGovernor(
address _target,
dt.Action _action,
address _account
) external {
bytes4 selector;
if (_action == dt.Action.Add) {
selector = IBridgeOwner.addGovernor.selector;
} else if (_action == dt.Action.Remove) {
selector = IBridgeOwner.removeGovernor.selector;
} else {
revert("invalid action");
}
bytes memory data = abi.encodeWithSelector(selector, _account);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit UpdateGovernorProposalCreated(proposalId, _target, _action, _account);
}
function proposeUpdateBridgeSupplyCap(
address _target,
address _bridge,
uint256 _cap
) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.updateBridgeSupplyCap.selector, _bridge, _cap);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit UpdateBridgeSupplyCapProposalCreated(proposalId, _target, _bridge, _cap);
}
function proposeSetBridgeTokenSwapCap(
address _target,
address _bridgeToken,
uint256 _swapCap
) external {
bytes memory data = abi.encodeWithSelector(IBridgeOwner.setBridgeTokenSwapCap.selector, _bridgeToken, _swapCap);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetBridgeTokenSwapCapProposalCreated(proposalId, _target, _bridgeToken, _swapCap);
}
}
================================================
FILE: contracts/governed-owner/proxies/CommonOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./OwnerProxyBase.sol";
import "../interfaces/ICommonOwner.sol";
import {SimpleGovernance as sg} from "../SimpleGovernance.sol";
import {OwnerDataTypes as dt} from "./OwnerDataTypes.sol";
abstract contract CommonOwnerProxy is OwnerProxyBase {
event TransferOwnershipProposalCreated(uint256 proposalId, address target, address newOwner);
event UpdatePauserProposalCreated(uint256 proposalId, address target, dt.Action action, address account);
function proposeTransferOwnership(address _target, address _newOwner) external {
bytes memory data = abi.encodeWithSelector(ICommonOwner.transferOwnership.selector, _newOwner);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit TransferOwnershipProposalCreated(proposalId, _target, _newOwner);
}
function proposeUpdatePauser(
address _target,
dt.Action _action,
address _account
) external {
bytes4 selector;
if (_action == dt.Action.Add) {
selector = ICommonOwner.addPauser.selector;
} else if (_action == dt.Action.Remove) {
selector = ICommonOwner.removePauser.selector;
} else {
revert("invalid action");
}
bytes memory data = abi.encodeWithSelector(selector, _account);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit UpdatePauserProposalCreated(proposalId, _target, _action, _account);
}
}
================================================
FILE: contracts/governed-owner/proxies/MessageOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./OwnerProxyBase.sol";
import "../interfaces/IMessageOwner.sol";
import {SimpleGovernance as sg} from "../SimpleGovernance.sol";
import {OwnerDataTypes as dt} from "./OwnerDataTypes.sol";
abstract contract MessageOwnerProxy is OwnerProxyBase {
event SetMsgFeeProposalCreated(uint256 proposalId, address target, dt.MsgFeeType feeType, uint256 fee);
event SetBridgeAddressProposalCreated(
uint256 proposalId,
address target,
dt.BridgeType bridgeType,
address bridgeAddr
);
event SetPreExecuteMessageGasUsageProposalCreated(uint256 proposalId, address target, uint256 usage);
function proposeSetMsgFee(
address _target,
dt.MsgFeeType _feeType,
uint256 _fee
) external {
bytes4 selector;
if (_feeType == dt.MsgFeeType.PerByte) {
selector = IMessageOwner.setFeePerByte.selector;
} else if (_feeType == dt.MsgFeeType.Base) {
selector = IMessageOwner.setFeeBase.selector;
} else {
revert("invalid fee type");
}
bytes memory data = abi.encodeWithSelector(selector, _fee);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit SetMsgFeeProposalCreated(proposalId, _target, _feeType, _fee);
}
function proposeSetBridgeAddress(
address _target,
dt.BridgeType _bridgeType,
address _bridgeAddr
) external {
bytes4 selector;
if (_bridgeType == dt.BridgeType.Liquidity) {
selector = IMessageOwner.setLiquidityBridge.selector;
} else if (_bridgeType == dt.BridgeType.PegBridge) {
selector = IMessageOwner.setPegBridge.selector;
} else if (_bridgeType == dt.BridgeType.PegVault) {
selector = IMessageOwner.setPegVault.selector;
} else if (_bridgeType == dt.BridgeType.PegBridgeV2) {
selector = IMessageOwner.setPegBridgeV2.selector;
} else if (_bridgeType == dt.BridgeType.PegVaultV2) {
selector = IMessageOwner.setPegVaultV2.selector;
} else {
revert("invalid bridge type");
}
bytes memory data = abi.encodeWithSelector(selector, _bridgeAddr);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetBridgeAddressProposalCreated(proposalId, _target, _bridgeType, _bridgeAddr);
}
function proposeSetPreExecuteMessageGasUsage(address _target, uint256 _usage) external {
bytes memory data = abi.encodeWithSelector(IMessageOwner.setPreExecuteMessageGasUsage.selector, _usage);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetPreExecuteMessageGasUsageProposalCreated(proposalId, _target, _usage);
}
}
================================================
FILE: contracts/governed-owner/proxies/OwnerDataTypes.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
library OwnerDataTypes {
enum Action {
Set,
Add,
Remove
}
enum MsgFeeType {
PerByte,
Base
}
enum BridgeType {
Liquidity,
PegBridge,
PegVault,
PegBridgeV2,
PegVaultV2
}
}
================================================
FILE: contracts/governed-owner/proxies/OwnerProxyBase.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "../SimpleGovernance.sol";
abstract contract OwnerProxyBase {
SimpleGovernance public gov;
address private initializer;
constructor(address _initializer) {
initializer = _initializer;
}
function initGov(SimpleGovernance _gov) public {
require(msg.sender == initializer, "only initializer can init");
require(address(gov) == address(0), "gov addr already set");
gov = _gov;
}
}
================================================
FILE: contracts/governed-owner/proxies/SgnOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./OwnerProxyBase.sol";
import "../interfaces/ISgnOwner.sol";
import {SimpleGovernance as sg} from "../SimpleGovernance.sol";
import {OwnerDataTypes as dt} from "./OwnerDataTypes.sol";
abstract contract SgnOwnerProxy is OwnerProxyBase {
event SetWhitelistEnableProposalCreated(uint256 proposalId, address target, bool enabled);
event UpdateWhitelistedProposalCreated(uint256 proposalId, address target, dt.Action action, address account);
event SetGovContractProposalCreated(uint256 proposalId, address target, address addr);
event SetRewardContractProposalCreated(uint256 proposalId, address target, address addr);
event SetMaxSlashFactorProposalCreated(uint256 proposalId, address target, uint256 maxSlashFactor);
event DrainTokenProposalCreated(uint256 proposalId, address target, address token, uint256 amount);
function proposeSetWhitelistEnable(address _target, bool _enable) external {
bytes memory data = abi.encodeWithSelector(ISgnOwner.setWhitelistEnabled.selector, _enable);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetWhitelistEnableProposalCreated(proposalId, _target, _enable);
}
function proposeUpdateWhitelisted(
address _target,
dt.Action _action,
address _account
) external {
bytes4 selector;
if (_action == dt.Action.Add) {
selector = ISgnOwner.addWhitelisted.selector;
} else if (_action == dt.Action.Remove) {
selector = ISgnOwner.removeWhitelisted.selector;
} else {
revert("invalid action");
}
bytes memory data = abi.encodeWithSelector(selector, _account);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalFastPass);
emit UpdateWhitelistedProposalCreated(proposalId, _target, _action, _account);
}
function proposeSetGovContract(address _target, address _addr) external {
bytes memory data = abi.encodeWithSelector(ISgnOwner.setGovContract.selector, _addr);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetGovContractProposalCreated(proposalId, _target, _addr);
}
function proposeSetRewardContract(address _target, address _addr) external {
bytes memory data = abi.encodeWithSelector(ISgnOwner.setRewardContract.selector, _addr);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetRewardContractProposalCreated(proposalId, _target, _addr);
}
function proposeSetMaxSlashFactor(address _target, uint256 _maxSlashFactor) external {
bytes memory data = abi.encodeWithSelector(ISgnOwner.setMaxSlashFactor.selector, _maxSlashFactor);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit SetMaxSlashFactorProposalCreated(proposalId, _target, _maxSlashFactor);
}
function proposeDrainToken(
address _target,
address _token,
uint256 _amount
) external {
bytes memory data;
if (_token == address(0)) {
data = abi.encodeWithSelector(bytes4(keccak256(bytes("drainToken(uint256"))), _amount);
} else {
data = abi.encodeWithSelector(bytes4(keccak256(bytes("drainToken(address,uint256"))), _token, _amount);
}
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit DrainTokenProposalCreated(proposalId, _target, _token, _amount);
}
}
================================================
FILE: contracts/governed-owner/proxies/UpgradeableOwnerProxy.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./OwnerProxyBase.sol";
import "../interfaces/IUpgradeableOwner.sol";
import {SimpleGovernance as sg} from "../SimpleGovernance.sol";
import {OwnerDataTypes as dt} from "./OwnerDataTypes.sol";
abstract contract UpgradeableOwnerProxy is OwnerProxyBase {
event ChangeProxyAdminProposalCreated(uint256 proposalId, address target, address proxy, address newAdmin);
event UpgradeProposalCreated(uint256 proposalId, address target, address proxy, address implementation);
event UpgradeAndCallProposalCreated(
uint256 proposalId,
address target,
address proxy,
address implementation,
bytes data
);
event UpgradeToProposalCreated(uint256 proposalId, address target, address implementation);
event UpgradeToAndCallProposalCreated(uint256 proposalId, address target, address implementation, bytes data);
function proposeChangeProxyAdmin(
address _target,
address _proxy,
address _newAdmin
) external {
bytes memory data = abi.encodeWithSelector(IUpgradeableOwner.changeProxyAdmin.selector, _proxy, _newAdmin);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit ChangeProxyAdminProposalCreated(proposalId, _target, _proxy, _newAdmin);
}
function proposeUpgrade(
address _target,
address _proxy,
address _implementation
) external {
bytes memory data = abi.encodeWithSelector(IUpgradeableOwner.upgrade.selector, _proxy, _implementation);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit UpgradeProposalCreated(proposalId, _target, _proxy, _implementation);
}
function proposeUpgradeAndCall(
address _target,
address _proxy,
address _implementation,
bytes calldata _data
) external {
bytes memory data = abi.encodeWithSelector(
IUpgradeableOwner.upgradeAndCall.selector,
_proxy,
_implementation,
_data
);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit UpgradeAndCallProposalCreated(proposalId, _target, _proxy, _implementation, _data);
}
function proposeUpgradeTo(address _target, address _implementation) external {
bytes memory data = abi.encodeWithSelector(IUpgradeableOwner.upgradeTo.selector, _implementation);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit UpgradeToProposalCreated(proposalId, _target, _implementation);
}
function proposeUpgradeToAndCall(
address _target,
address _implementation,
bytes calldata _data
) external {
bytes memory data = abi.encodeWithSelector(IUpgradeableOwner.upgradeToAndCall.selector, _implementation, _data);
uint256 proposalId = gov.createProposal(msg.sender, _target, data, sg.ProposalType.ExternalDefault);
emit UpgradeToAndCallProposalCreated(proposalId, _target, _implementation, _data);
}
}
================================================
FILE: contracts/integration-examples/ContractAsLP.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/IPool.sol";
import "../interfaces/IWithdrawInbox.sol";
import "../safeguard/Pauser.sol";
/**
* @title Example contract to provide liquidity to {Bridge}. Supports withdrawing liquidity via {WithdrawInbox}.
*/
contract ContractAsLP is ReentrancyGuard, Pauser {
using SafeERC20 for IERC20;
address public bridge;
address public inbox;
event Deposited(address depositor, address token, uint256 amount);
constructor(address _bridge, address _inbox) {
bridge = _bridge;
inbox = _inbox;
}
/**
* @notice Deposit tokens.
* @param _token The deposited token address.
* @param _amount The amount to deposit.
*/
function deposit(address _token, uint256 _amount) external nonReentrant whenNotPaused onlyOwner {
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposited(msg.sender, _token, _amount);
}
/**
* @notice Add liquidity to the pool-based bridge.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The address of the token.
* @param _amount The amount to add.
*/
function addLiquidity(address _token, uint256 _amount) external whenNotPaused onlyOwner {
require(IERC20(_token).balanceOf(address(this)) >= _amount, "insufficient balance");
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IPool(bridge).addLiquidity(_token, _amount);
}
/**
* @notice Withdraw liquidity from the pool-based bridge.
* NOTE: Each of your withdrawal request should have different _wdSeq.
* NOTE: Tokens to withdraw within one withdrawal request should have the same symbol.
* @param _wdSeq The unique sequence number to identify this withdrawal request.
* @param _receiver The receiver address on _toChain.
* @param _toChain The chain Id to receive the withdrawn tokens.
* @param _fromChains The chain Ids to withdraw tokens.
* @param _tokens The token to withdraw on each fromChain.
* @param _ratios The withdrawal ratios of each token.
* @param _slippages The max slippages of each token for cross-chain withdraw.
*/
function withdraw(
uint64 _wdSeq,
address _receiver,
uint64 _toChain,
uint64[] calldata _fromChains,
address[] calldata _tokens,
uint32[] calldata _ratios,
uint32[] calldata _slippages
) external whenNotPaused onlyOwner {
IWithdrawInbox(inbox).withdraw(_wdSeq, _receiver, _toChain, _fromChains, _tokens, _ratios, _slippages);
}
}
================================================
FILE: contracts/integration-examples/ContractAsSender.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../libraries/BridgeTransferLib.sol";
import "../safeguard/Pauser.sol";
/**
* @title Example contract to send cBridge transfers. Supports the liquidity pool-based {Bridge}, the {OriginalTokenVault} for pegged
* deposit and the {PeggedTokenBridge} for pegged burn. Includes handling of refunds for failed transfers.
* @notice For the bad Bridge.send/PeggedTokenBridge.deposit of native token(eg.ETH) or wrapped native token(eg.WETH),
* its refund asset depends on whether the nativeWrap of Bridge/PeggedTokenBridge is set or not AT THE MOMENT OF REFUNDING.
* If the nativeWrap is set, the refund asset would always be native token (eg.ETH), even though the original sending asset
* is wrapped native token. If the nativeWrap isn't set, the refund asset would always be wrapped native token.
*/
contract ContractAsSender is ReentrancyGuard, Pauser {
using SafeERC20 for IERC20;
mapping(BridgeTransferLib.BridgeSendType => address) public bridges;
mapping(bytes32 => address) public records;
address public nativeWrap;
uint256 public nativeTokenTransferGas = 50000;
event Deposited(address depositor, address token, uint256 amount);
event BridgeUpdated(BridgeTransferLib.BridgeSendType bridgeSendType, address bridgeAddr);
/**
* @notice Send a cross-chain transfer either via liquidity pool-based bridge or in form of mint/burn.
* @param _receiver The address of the receiver.
* @param _token The address of the token.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {BridgeSendType.Liquidity}.
* @param _bridgeSendType The type of bridge used by this transfer. One of the {BridgeSendType} enum.
*/
function transfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage, // slippage * 1M, eg. 0.5% -> 5000
BridgeTransferLib.BridgeSendType _bridgeSendType
) external nonReentrant whenNotPaused onlyOwner returns (bytes32) {
address _bridgeAddr = bridges[_bridgeSendType];
require(_bridgeAddr != address(0), "unknown bridge type");
bytes32 transferId = BridgeTransferLib.sendTransfer(
_receiver,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_bridgeSendType,
_bridgeAddr
);
require(records[transferId] == address(0), "record exists");
records[transferId] = msg.sender;
return transferId;
}
/**
* @notice Refund a failed cross-chain transfer.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeSendType The type of bridge used by this failed transfer. One of the {BridgeSendType} enum.
*/
function refund(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
BridgeTransferLib.BridgeSendType _bridgeSendType
) external nonReentrant whenNotPaused onlyOwner returns (bytes32) {
address _bridgeAddr = bridges[_bridgeSendType];
require(_bridgeAddr != address(0), "unknown bridge type");
BridgeTransferLib.ReceiveInfo memory refundInfo = BridgeTransferLib.receiveTransfer(
_request,
_sigs,
_signers,
_powers,
BridgeTransferLib.bridgeRefundType(_bridgeSendType),
_bridgeAddr
);
require(refundInfo.receiver == address(this), "invalid refund");
address _receiver = records[refundInfo.refid];
require(_receiver != address(0), "unknown transfer id or already refunded");
delete records[refundInfo.refid];
_sendToken(_receiver, refundInfo.token, refundInfo.amount);
return refundInfo.transferId;
}
/**
* @notice Send token to user. For native token and wrapped native token, this contract may not have enough _token to
* send to _receiver. This may caused by others refund an original transfer that is sent from this contract via cBridge
* contract right before you call refund function of this contract and then the nativeWrap of cBridge contract is
* modified right after that the refund triggered by that guy completes.
* As a consequence, native token and wrapped native token possessed by this contract are mixed. But don't worry,
* the total sum of two tokens keeps correct. So in order to avoid deadlocking any token, we'd better have a
* balance check before sending out native token or wrapped native token. If the balance of _token is not sufficient,
* we change to sent the other token.
*/
function _sendToken(
address _receiver,
address _token,
uint256 _amount
) internal {
if (_token == address(0)) {
// refund asset is ETH
if (address(this).balance >= _amount) {
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
} else {
// in case of refund asset is WETH
IERC20(_token).safeTransfer(_receiver, _amount);
}
} else if (_token == nativeWrap) {
// refund asset is WETH
if (IERC20(_token).balanceOf(address(this)) >= _amount) {
IERC20(_token).safeTransfer(_receiver, _amount);
} else {
// in case of refund asset is ETH
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
}
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
// ----------------------Admin operation-----------------------
/**
* @notice Lock tokens.
* @param _token The deposited token address.
* @param _amount The amount to deposit.
*/
function deposit(address _token, uint256 _amount) external nonReentrant whenNotPaused onlyOwner {
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposited(msg.sender, _token, _amount);
}
function setBridgeAddress(BridgeTransferLib.BridgeSendType _bridgeSendType, address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
bridges[_bridgeSendType] = _addr;
emit BridgeUpdated(_bridgeSendType, _addr);
}
// set nativeWrap
function setWrap(address _weth) external onlyOwner {
nativeWrap = _weth;
}
// setNativeTransferGasUsed, native transfer will use this config.
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyOwner {
nativeTokenTransferGas = _gasUsed;
}
// This is needed to receive ETH if a refund asset is ETH
receive() external payable {}
}
================================================
FILE: contracts/interfaces/IBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IBridge {
function send(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage
) external;
function sendNative(
address _receiver,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage
) external payable;
function relay(
bytes calldata _relayRequest,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
function transfers(bytes32 transferId) external view returns (bool);
function withdraws(bytes32 withdrawId) external view returns (bool);
function withdraw(
bytes calldata _wdmsg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
/**
* @notice Verifies that a message is signed by a quorum among the signers.
* @param _msg signed message
* @param _sigs list of signatures sorted by signer addresses in ascending order
* @param _signers sorted list of current signers
* @param _powers powers of current signers
*/
function verifySigs(
bytes memory _msg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external view;
}
================================================
FILE: contracts/interfaces/ICircleBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface ICircleBridge {
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given burnToken is not supported
* - given destinationDomain has no CircleBridge registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - MessageTransmitter returns false or reverts.
* @param _amount amount of tokens to burn
* @param _destinationDomain destination domain (ETH = 0, AVAX = 1)
* @param _mintRecipient address of mint recipient on destination domain
* @param _burnToken address of contract to burn deposited tokens, on local domain
* @return _nonce unique nonce reserved by message
*/
function depositForBurn(
uint256 _amount,
uint32 _destinationDomain,
bytes32 _mintRecipient,
address _burnToken
) external returns (uint64 _nonce);
/**
* @notice Deposits and burns tokens from sender to be minted on destination domain.
* Emits a `DepositForBurn` event.
* @dev reverts if:
* - given burnToken is not supported
* - given destinationDomain has no TokenMessenger registered
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
* to this contract is less than `amount`.
* - burn() reverts. For example, if `amount` is 0.
* - maxFee is greater than or equal to `amount`.
* - MessageTransmitterV2#sendMessage reverts.
* @param amount amount of tokens to burn
* @param destinationDomain destination domain to receive message on
* @param mintRecipient address of mint recipient on destination domain
* @param burnToken token to burn `amount` of, on local domain
* @param destinationCaller authorized caller on the destination domain, as bytes32. If equal to bytes32(0),
* any address can broadcast the message.
* @param maxFee maximum fee to pay on the destination domain, specified in units of burnToken
* @param minFinalityThreshold the minimum finality at which a burn message will be attested to.
*/
function depositForBurn(
uint256 amount,
uint32 destinationDomain,
bytes32 mintRecipient,
address burnToken,
bytes32 destinationCaller,
uint256 maxFee,
uint32 minFinalityThreshold
) external;
}
================================================
FILE: contracts/interfaces/IDelayedTransfer.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
interface IDelayedTransfer {
struct delayedTransfer {
address receiver;
address token;
uint256 amount;
uint256 timestamp;
}
function delayedTransfers(bytes32 transferId) external view returns (delayedTransfer memory);
}
================================================
FILE: contracts/interfaces/IOriginalTokenVault.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IOriginalTokenVault {
/**
* @notice Lock original tokens to trigger mint at a remote chain's PeggedTokenBridge
* @param _token local token address
* @param _amount locked token amount
* @param _mintChainId destination chainId to mint tokens
* @param _mintAccount destination account to receive minted tokens
* @param _nonce user input to guarantee unique depositId
*/
function deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external;
/**
* @notice Lock native token as original token to trigger mint at a remote chain's PeggedTokenBridge
* @param _amount locked token amount
* @param _mintChainId destination chainId to mint tokens
* @param _mintAccount destination account to receive minted tokens
* @param _nonce user input to guarantee unique depositId
*/
function depositNative(
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external payable;
/**
* @notice Withdraw locked original tokens triggered by a burn at a remote chain's PeggedTokenBridge.
* @param _request The serialized Withdraw protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
function records(bytes32 recordId) external view returns (bool);
}
================================================
FILE: contracts/interfaces/IOriginalTokenVaultV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IOriginalTokenVaultV2 {
/**
* @notice Lock original tokens to trigger mint at a remote chain's PeggedTokenBridge
* @param _token local token address
* @param _amount locked token amount
* @param _mintChainId destination chainId to mint tokens
* @param _mintAccount destination account to receive minted tokens
* @param _nonce user input to guarantee unique depositId
*/
function deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external returns (bytes32);
/**
* @notice Lock native token as original token to trigger mint at a remote chain's PeggedTokenBridge
* @param _amount locked token amount
* @param _mintChainId destination chainId to mint tokens
* @param _mintAccount destination account to receive minted tokens
* @param _nonce user input to guarantee unique depositId
*/
function depositNative(
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external payable returns (bytes32);
/**
* @notice Withdraw locked original tokens triggered by a burn at a remote chain's PeggedTokenBridge.
* @param _request The serialized Withdraw protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external returns (bytes32);
function records(bytes32 recordId) external view returns (bool);
}
================================================
FILE: contracts/interfaces/IPeggedToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IPeggedToken {
function mint(address _to, uint256 _amount) external;
function burn(address _from, uint256 _amount) external;
}
================================================
FILE: contracts/interfaces/IPeggedTokenBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IPeggedTokenBridge {
/**
* @notice Burn tokens to trigger withdrawal at a remote chain's OriginalTokenVault
* @param _token local token address
* @param _amount locked token amount
* @param _withdrawAccount account who withdraw original tokens on the remote chain
* @param _nonce user input to guarantee unique depositId
*/
function burn(
address _token,
uint256 _amount,
address _withdrawAccount,
uint64 _nonce
) external;
/**
* @notice Mint tokens triggered by deposit at a remote chain's OriginalTokenVault.
* @param _request The serialized Mint protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function mint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
function records(bytes32 recordId) external view returns (bool);
}
================================================
FILE: contracts/interfaces/IPeggedTokenBridgeV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IPeggedTokenBridgeV2 {
/**
* @notice Burn pegged tokens to trigger a cross-chain withdrawal of the original tokens at a remote chain's
* OriginalTokenVault, or mint at another remote chain
* @param _token The pegged token address.
* @param _amount The amount to burn.
* @param _toChainId If zero, withdraw from original vault; otherwise, the remote chain to mint tokens.
* @param _toAccount The account to receive tokens on the remote chain
* @param _nonce A number to guarantee unique depositId. Can be timestamp in practice.
*/
function burn(
address _token,
uint256 _amount,
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) external returns (bytes32);
// same with `burn` above, use openzeppelin ERC20Burnable interface
function burnFrom(
address _token,
uint256 _amount,
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) external returns (bytes32);
/**
* @notice Mint tokens triggered by deposit at a remote chain's OriginalTokenVault.
* @param _request The serialized Mint protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function mint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external returns (bytes32);
function records(bytes32 recordId) external view returns (bool);
}
================================================
FILE: contracts/interfaces/IPeggedTokenBurnFrom.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
// used for pegged token with openzeppelin ERC20Burnable interface
// only compatible with PeggedTokenBridgeV2
interface IPeggedTokenBurnFrom {
function mint(address _to, uint256 _amount) external;
function burnFrom(address _from, uint256 _amount) external;
}
================================================
FILE: contracts/interfaces/IPool.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IPool {
function addLiquidity(address _token, uint256 _amount) external;
function withdraws(bytes32 withdrawId) external view returns (bool);
function withdraw(
bytes calldata _wdmsg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
}
================================================
FILE: contracts/interfaces/ISigsVerifier.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface ISigsVerifier {
/**
* @notice Verifies that a message is signed by a quorum among the signers.
* @param _msg signed message
* @param _sigs list of signatures sorted by signer addresses in ascending order
* @param _signers sorted list of current signers
* @param _powers powers of current signers
*/
function verifySigs(
bytes memory _msg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external view;
}
================================================
FILE: contracts/interfaces/IUniswapV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IUniswapV2 {
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
}
================================================
FILE: contracts/interfaces/IWETH.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IWETH {
function deposit() external payable;
function withdraw(uint256) external;
}
================================================
FILE: contracts/interfaces/IWithdrawInbox.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IWithdrawInbox {
function withdraw(
uint64 _wdSeq,
address _receiver,
uint64 _toChain,
uint64[] calldata _fromChains,
address[] calldata _tokens,
uint32[] calldata _ratios,
uint32[] calldata _slippages
) external;
}
================================================
FILE: contracts/libraries/BridgeTransferLib.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./PbBridge.sol";
import "./PbPegged.sol";
import "./PbPool.sol";
import "../interfaces/IBridge.sol";
import "../interfaces/IOriginalTokenVault.sol";
import "../interfaces/IOriginalTokenVaultV2.sol";
import "../interfaces/IPeggedTokenBridge.sol";
import "../interfaces/IPeggedTokenBridgeV2.sol";
interface INativeWrap {
function nativeWrap() external view returns (address);
}
library BridgeTransferLib {
using SafeERC20 for IERC20;
enum BridgeSendType {
Null,
Liquidity,
PegDeposit,
PegBurn,
PegV2Deposit,
PegV2Burn,
PegV2BurnFrom
}
enum BridgeReceiveType {
Null,
LqRelay,
LqWithdraw,
PegMint,
PegWithdraw,
PegV2Mint,
PegV2Withdraw
}
struct ReceiveInfo {
bytes32 transferId;
address receiver;
address token; // 0 address for native token
uint256 amount;
bytes32 refid; // reference id, e.g., srcTransferId for refund
}
// ============== Internal library functions called by apps ==============
/**
* @notice Send a cross-chain transfer of ERC20 token either via liquidity pool-based bridge or in the form of pegged mint / burn.
* @param _receiver The address of the receiver.
* @param _token The address of the token.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {BridgeSendType.Liquidity}.
* @param _bridgeSendType The type of the bridge used by this transfer. One of the {BridgeSendType} enum.
* @param _bridgeAddr The address of the bridge used.
*/
function sendTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage, // slippage * 1M, eg. 0.5% -> 5000
BridgeSendType _bridgeSendType,
address _bridgeAddr
) internal returns (bytes32) {
bytes32 transferId;
IERC20(_token).safeIncreaseAllowance(_bridgeAddr, _amount);
if (_bridgeSendType == BridgeSendType.Liquidity) {
IBridge(_bridgeAddr).send(_receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage);
transferId = keccak256(
abi.encodePacked(address(this), _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid))
);
} else if (_bridgeSendType == BridgeSendType.PegDeposit) {
IOriginalTokenVault(_bridgeAddr).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
transferId = keccak256(
abi.encodePacked(address(this), _token, _amount, _dstChainId, _receiver, _nonce, uint64(block.chainid))
);
} else if (_bridgeSendType == BridgeSendType.PegBurn) {
IPeggedTokenBridge(_bridgeAddr).burn(_token, _amount, _receiver, _nonce);
transferId = keccak256(
abi.encodePacked(address(this), _token, _amount, _receiver, _nonce, uint64(block.chainid))
);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(_bridgeAddr, 0);
} else if (_bridgeSendType == BridgeSendType.PegV2Deposit) {
transferId = IOriginalTokenVaultV2(_bridgeAddr).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
} else if (_bridgeSendType == BridgeSendType.PegV2Burn) {
transferId = IPeggedTokenBridgeV2(_bridgeAddr).burn(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(_bridgeAddr, 0);
} else if (_bridgeSendType == BridgeSendType.PegV2BurnFrom) {
transferId = IPeggedTokenBridgeV2(_bridgeAddr).burnFrom(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(_bridgeAddr, 0);
} else {
revert("bridge send type not supported");
}
return transferId;
}
/**
* @notice Send a cross-chain transfer of native token either via liquidity pool-based bridge or in the form of pegged mint / burn.
* @param _receiver The address of the receiver.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {BridgeSendType.Liquidity}.
* @param _bridgeSendType The type of the bridge used by this transfer. One of the {BridgeSendType} enum.
* @param _bridgeAddr The address of the bridge used.
*/
function sendNativeTransfer(
address _receiver,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage, // slippage * 1M, eg. 0.5% -> 5000
BridgeSendType _bridgeSendType,
address _bridgeAddr
) internal returns (bytes32) {
require(
_bridgeSendType == BridgeSendType.Liquidity ||
_bridgeSendType == BridgeSendType.PegDeposit ||
_bridgeSendType == BridgeSendType.PegV2Deposit,
"Lib: invalid bridge send type"
);
address _token = INativeWrap(_bridgeAddr).nativeWrap();
bytes32 transferId;
if (_bridgeSendType == BridgeSendType.Liquidity) {
IBridge(_bridgeAddr).sendNative{value: msg.value}(_receiver, _amount, _dstChainId, _nonce, _maxSlippage);
transferId = keccak256(
abi.encodePacked(address(this), _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid))
);
} else if (_bridgeSendType == BridgeSendType.PegDeposit) {
IOriginalTokenVault(_bridgeAddr).depositNative{value: msg.value}(_amount, _dstChainId, _receiver, _nonce);
transferId = keccak256(
abi.encodePacked(address(this), _token, _amount, _dstChainId, _receiver, _nonce, uint64(block.chainid))
);
} else {
// _bridgeSendType == BridgeSendType.PegV2Deposit
transferId = IOriginalTokenVaultV2(_bridgeAddr).depositNative{value: msg.value}(
_amount,
_dstChainId,
_receiver,
_nonce
);
}
return transferId;
}
/**
* @notice Receive a cross-chain transfer.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeReceiveType The type of the received transfer. One of the {BridgeReceiveType} enum.
* @param _bridgeAddr The address of the bridge used.
*/
function receiveTransfer(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
BridgeReceiveType _bridgeReceiveType,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
if (_bridgeReceiveType == BridgeReceiveType.LqRelay) {
return receiveLiquidityRelay(_request, _sigs, _signers, _powers, _bridgeAddr);
} else if (_bridgeReceiveType == BridgeReceiveType.LqWithdraw) {
return receiveLiquidityWithdraw(_request, _sigs, _signers, _powers, _bridgeAddr);
} else if (_bridgeReceiveType == BridgeReceiveType.PegWithdraw) {
return receivePegWithdraw(_request, _sigs, _signers, _powers, _bridgeAddr);
} else if (_bridgeReceiveType == BridgeReceiveType.PegMint) {
return receivePegMint(_request, _sigs, _signers, _powers, _bridgeAddr);
} else if (_bridgeReceiveType == BridgeReceiveType.PegV2Withdraw) {
return receivePegV2Withdraw(_request, _sigs, _signers, _powers, _bridgeAddr);
} else if (_bridgeReceiveType == BridgeReceiveType.PegV2Mint) {
return receivePegV2Mint(_request, _sigs, _signers, _powers, _bridgeAddr);
} else {
revert("bridge receive type not supported");
}
}
/**
* @notice Receive a liquidity bridge relay.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of liquidity bridge.
*/
function receiveLiquidityRelay(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbBridge.Relay memory request = PbBridge.decRelay(_request);
recv.transferId = keccak256(
abi.encodePacked(
request.sender,
request.receiver,
request.token,
request.amount,
request.srcChainId,
uint64(block.chainid),
request.srcTransferId
)
);
recv.refid = request.srcTransferId;
recv.receiver = request.receiver;
recv.token = request.token;
recv.amount = request.amount;
if (!IBridge(_bridgeAddr).transfers(recv.transferId)) {
IBridge(_bridgeAddr).relay(_request, _sigs, _signers, _powers);
}
return recv;
}
/**
* @notice Receive a liquidity bridge withdrawal.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of liquidity bridge.
*/
function receiveLiquidityWithdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbPool.WithdrawMsg memory request = PbPool.decWithdrawMsg(_request);
recv.transferId = keccak256(
abi.encodePacked(request.chainid, request.seqnum, request.receiver, request.token, request.amount)
);
recv.refid = request.refid;
recv.receiver = request.receiver;
if (INativeWrap(_bridgeAddr).nativeWrap() == request.token) {
recv.token = address(0);
} else {
recv.token = request.token;
}
recv.amount = request.amount;
if (!IBridge(_bridgeAddr).withdraws(recv.transferId)) {
IBridge(_bridgeAddr).withdraw(_request, _sigs, _signers, _powers);
}
return recv;
}
/**
* @notice Receive an OriginalTokenVault withdrawal.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of OriginalTokenVault.
*/
function receivePegWithdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbPegged.Withdraw memory request = PbPegged.decWithdraw(_request);
recv.transferId = keccak256(
abi.encodePacked(
request.receiver,
request.token,
request.amount,
request.burnAccount,
request.refChainId,
request.refId
)
);
recv.refid = request.refId;
recv.receiver = request.receiver;
if (INativeWrap(_bridgeAddr).nativeWrap() == request.token) {
recv.token = address(0);
} else {
recv.token = request.token;
}
recv.amount = request.amount;
if (!IOriginalTokenVault(_bridgeAddr).records(recv.transferId)) {
IOriginalTokenVault(_bridgeAddr).withdraw(_request, _sigs, _signers, _powers);
}
return recv;
}
/**
* @notice Receive a PeggedTokenBridge mint.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of PeggedTokenBridge.
*/
function receivePegMint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbPegged.Mint memory request = PbPegged.decMint(_request);
recv.transferId = keccak256(
abi.encodePacked(
request.account,
request.token,
request.amount,
request.depositor,
request.refChainId,
request.refId
)
);
recv.refid = request.refId;
recv.receiver = request.account;
recv.token = request.token;
recv.amount = request.amount;
if (!IPeggedTokenBridge(_bridgeAddr).records(recv.transferId)) {
IPeggedTokenBridge(_bridgeAddr).mint(_request, _sigs, _signers, _powers);
}
return recv;
}
/**
* @notice Receive an OriginalTokenVaultV2 withdrawal.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A request must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of OriginalTokenVaultV2.
*/
function receivePegV2Withdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbPegged.Withdraw memory request = PbPegged.decWithdraw(_request);
if (IOriginalTokenVaultV2(_bridgeAddr).records(request.refId)) {
recv.transferId = keccak256(
abi.encodePacked(
request.receiver,
request.token,
request.amount,
request.burnAccount,
request.refChainId,
request.refId,
_bridgeAddr
)
);
} else {
recv.transferId = IOriginalTokenVaultV2(_bridgeAddr).withdraw(_request, _sigs, _signers, _powers);
}
recv.refid = request.refId;
recv.receiver = request.receiver;
if (INativeWrap(_bridgeAddr).nativeWrap() == request.token) {
recv.token = address(0);
} else {
recv.token = request.token;
}
recv.amount = request.amount;
return recv;
}
/**
* @notice Receive a PeggedTokenBridgeV2 mint.
* @param _request The serialized request protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A request must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
* @param _bridgeAddr The address of PeggedTokenBridgeV2.
*/
function receivePegV2Mint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
address _bridgeAddr
) internal returns (ReceiveInfo memory) {
ReceiveInfo memory recv;
PbPegged.Mint memory request = PbPegged.decMint(_request);
if (IPeggedTokenBridgeV2(_bridgeAddr).records(request.refId)) {
recv.transferId = keccak256(
abi.encodePacked(
request.account,
request.token,
request.amount,
request.depositor,
request.refChainId,
request.refId,
_bridgeAddr
)
);
} else {
recv.transferId = IPeggedTokenBridgeV2(_bridgeAddr).mint(_request, _sigs, _signers, _powers);
}
recv.refid = request.refId;
recv.receiver = request.account;
recv.token = request.token;
recv.amount = request.amount;
return recv;
}
function bridgeRefundType(BridgeSendType _bridgeSendType) internal pure returns (BridgeReceiveType) {
if (_bridgeSendType == BridgeSendType.Liquidity) {
return BridgeReceiveType.LqWithdraw;
}
if (_bridgeSendType == BridgeSendType.PegDeposit) {
return BridgeReceiveType.PegWithdraw;
}
if (_bridgeSendType == BridgeSendType.PegBurn) {
return BridgeReceiveType.PegMint;
}
if (_bridgeSendType == BridgeSendType.PegV2Deposit) {
return BridgeReceiveType.PegV2Withdraw;
}
if (_bridgeSendType == BridgeSendType.PegV2Burn || _bridgeSendType == BridgeSendType.PegV2BurnFrom) {
return BridgeReceiveType.PegV2Mint;
}
return BridgeReceiveType.Null;
}
}
================================================
FILE: contracts/libraries/Pb.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
// runtime proto sol library
library Pb {
enum WireType {
Varint,
Fixed64,
LengthDelim,
StartGroup,
EndGroup,
Fixed32
}
struct Buffer {
uint256 idx; // the start index of next read. when idx=b.length, we're done
bytes b; // hold serialized proto msg, readonly
}
// create a new in-memory Buffer object from raw msg bytes
function fromBytes(bytes memory raw) internal pure returns (Buffer memory buf) {
buf.b = raw;
buf.idx = 0;
}
// whether there are unread bytes
function hasMore(Buffer memory buf) internal pure returns (bool) {
return buf.idx < buf.b.length;
}
// decode current field number and wiretype
function decKey(Buffer memory buf) internal pure returns (uint256 tag, WireType wiretype) {
uint256 v = decVarint(buf);
tag = v / 8;
wiretype = WireType(v & 7);
}
// count tag occurrences, return an array due to no memory map support
// have to create array for (maxtag+1) size. cnts[tag] = occurrences
// should keep buf.idx unchanged because this is only a count function
function cntTags(Buffer memory buf, uint256 maxtag) internal pure returns (uint256[] memory cnts) {
uint256 originalIdx = buf.idx;
cnts = new uint256[](maxtag + 1); // protobuf's tags are from 1 rather than 0
uint256 tag;
WireType wire;
while (hasMore(buf)) {
(tag, wire) = decKey(buf);
cnts[tag] += 1;
skipValue(buf, wire);
}
buf.idx = originalIdx;
}
// read varint from current buf idx, move buf.idx to next read, return the int value
function decVarint(Buffer memory buf) internal pure returns (uint256 v) {
bytes10 tmp; // proto int is at most 10 bytes (7 bits can be used per byte)
bytes memory bb = buf.b; // get buf.b mem addr to use in assembly
v = buf.idx; // use v to save one additional uint variable
assembly {
tmp := mload(add(add(bb, 32), v)) // load 10 bytes from buf.b[buf.idx] to tmp
}
uint256 b; // store current byte content
v = 0; // reset to 0 for return value
for (uint256 i = 0; i < 10; i++) {
assembly {
b := byte(i, tmp) // don't use tmp[i] because it does bound check and costs extra
}
v |= (b & 0x7F) << (i * 7);
if (b & 0x80 == 0) {
buf.idx += i + 1;
return v;
}
}
revert(); // i=10, invalid varint stream
}
// read length delimited field and return bytes
function decBytes(Buffer memory buf) internal pure returns (bytes memory b) {
uint256 len = decVarint(buf);
uint256 end = buf.idx + len;
require(end <= buf.b.length); // avoid overflow
b = new bytes(len);
bytes memory bufB = buf.b; // get buf.b mem addr to use in assembly
uint256 bStart;
uint256 bufBStart = buf.idx;
assembly {
bStart := add(b, 32)
bufBStart := add(add(bufB, 32), bufBStart)
}
for (uint256 i = 0; i < len; i += 32) {
assembly {
mstore(add(bStart, i), mload(add(bufBStart, i)))
}
}
buf.idx = end;
}
// return packed ints
function decPacked(Buffer memory buf) internal pure returns (uint256[] memory t) {
uint256 len = decVarint(buf);
uint256 end = buf.idx + len;
require(end <= buf.b.length); // avoid overflow
// array in memory must be init w/ known length
// so we have to create a tmp array w/ max possible len first
uint256[] memory tmp = new uint256[](len);
uint256 i = 0; // count how many ints are there
while (buf.idx < end) {
tmp[i] = decVarint(buf);
i++;
}
t = new uint256[](i); // init t with correct length
for (uint256 j = 0; j < i; j++) {
t[j] = tmp[j];
}
return t;
}
// move idx pass current value field, to beginning of next tag or msg end
function skipValue(Buffer memory buf, WireType wire) internal pure {
if (wire == WireType.Varint) {
decVarint(buf);
} else if (wire == WireType.LengthDelim) {
uint256 len = decVarint(buf);
buf.idx += len; // skip len bytes value data
require(buf.idx <= buf.b.length); // avoid overflow
} else {
revert();
} // unsupported wiretype
}
// type conversion help utils
function _bool(uint256 x) internal pure returns (bool v) {
return x != 0;
}
function _uint256(bytes memory b) internal pure returns (uint256 v) {
require(b.length <= 32); // b's length must be smaller than or equal to 32
assembly {
v := mload(add(b, 32))
} // load all 32bytes to v
v = v >> (8 * (32 - b.length)); // only first b.length is valid
}
function _address(bytes memory b) internal pure returns (address v) {
v = _addressPayable(b);
}
function _addressPayable(bytes memory b) internal pure returns (address payable v) {
require(b.length == 20);
//load 32bytes then shift right 12 bytes
assembly {
v := div(mload(add(b, 32)), 0x1000000000000000000000000)
}
}
function _bytes32(bytes memory b) internal pure returns (bytes32 v) {
require(b.length == 32);
assembly {
v := mload(add(b, 32))
}
}
// uint[] to uint8[]
function uint8s(uint256[] memory arr) internal pure returns (uint8[] memory t) {
t = new uint8[](arr.length);
for (uint256 i = 0; i < t.length; i++) {
t[i] = uint8(arr[i]);
}
}
function uint32s(uint256[] memory arr) internal pure returns (uint32[] memory t) {
t = new uint32[](arr.length);
for (uint256 i = 0; i < t.length; i++) {
t[i] = uint32(arr[i]);
}
}
function uint64s(uint256[] memory arr) internal pure returns (uint64[] memory t) {
t = new uint64[](arr.length);
for (uint256 i = 0; i < t.length; i++) {
t[i] = uint64(arr[i]);
}
}
function bools(uint256[] memory arr) internal pure returns (bool[] memory t) {
t = new bool[](arr.length);
for (uint256 i = 0; i < t.length; i++) {
t[i] = arr[i] != 0;
}
}
}
================================================
FILE: contracts/libraries/PbBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: bridge.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbBridge {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct Relay {
address sender; // tag: 1
address receiver; // tag: 2
address token; // tag: 3
uint256 amount; // tag: 4
uint64 srcChainId; // tag: 5
uint64 dstChainId; // tag: 6
bytes32 srcTransferId; // tag: 7
} // end struct Relay
function decRelay(bytes memory raw) internal pure returns (Relay memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.sender = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.receiver = Pb._address(buf.decBytes());
} else if (tag == 3) {
m.token = Pb._address(buf.decBytes());
} else if (tag == 4) {
m.amount = Pb._uint256(buf.decBytes());
} else if (tag == 5) {
m.srcChainId = uint64(buf.decVarint());
} else if (tag == 6) {
m.dstChainId = uint64(buf.decVarint());
} else if (tag == 7) {
m.srcTransferId = Pb._bytes32(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder Relay
}
================================================
FILE: contracts/libraries/PbFarming.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: contracts/libraries/proto/farming.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbFarming {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct FarmingRewards {
address recipient; // tag: 1
address[] tokenAddresses; // tag: 2
uint256[] cumulativeRewardAmounts; // tag: 3
} // end struct FarmingRewards
function decFarmingRewards(bytes memory raw) internal pure returns (FarmingRewards memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256[] memory cnts = buf.cntTags(3);
m.tokenAddresses = new address[](cnts[2]);
cnts[2] = 0; // reset counter for later use
m.cumulativeRewardAmounts = new uint256[](cnts[3]);
cnts[3] = 0; // reset counter for later use
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.recipient = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.tokenAddresses[cnts[2]] = Pb._address(buf.decBytes());
cnts[2]++;
} else if (tag == 3) {
m.cumulativeRewardAmounts[cnts[3]] = Pb._uint256(buf.decBytes());
cnts[3]++;
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder FarmingRewards
}
================================================
FILE: contracts/libraries/PbPegged.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: contracts/libraries/proto/pegged.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbPegged {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct Mint {
address token; // tag: 1
address account; // tag: 2
uint256 amount; // tag: 3
address depositor; // tag: 4
uint64 refChainId; // tag: 5
bytes32 refId; // tag: 6
} // end struct Mint
function decMint(bytes memory raw) internal pure returns (Mint memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.token = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.account = Pb._address(buf.decBytes());
} else if (tag == 3) {
m.amount = Pb._uint256(buf.decBytes());
} else if (tag == 4) {
m.depositor = Pb._address(buf.decBytes());
} else if (tag == 5) {
m.refChainId = uint64(buf.decVarint());
} else if (tag == 6) {
m.refId = Pb._bytes32(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder Mint
struct Withdraw {
address token; // tag: 1
address receiver; // tag: 2
uint256 amount; // tag: 3
address burnAccount; // tag: 4
uint64 refChainId; // tag: 5
bytes32 refId; // tag: 6
} // end struct Withdraw
function decWithdraw(bytes memory raw) internal pure returns (Withdraw memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.token = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.receiver = Pb._address(buf.decBytes());
} else if (tag == 3) {
m.amount = Pb._uint256(buf.decBytes());
} else if (tag == 4) {
m.burnAccount = Pb._address(buf.decBytes());
} else if (tag == 5) {
m.refChainId = uint64(buf.decVarint());
} else if (tag == 6) {
m.refId = Pb._bytes32(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder Withdraw
}
================================================
FILE: contracts/libraries/PbPool.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: contracts/libraries/proto/pool.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbPool {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct WithdrawMsg {
uint64 chainid; // tag: 1
uint64 seqnum; // tag: 2
address receiver; // tag: 3
address token; // tag: 4
uint256 amount; // tag: 5
bytes32 refid; // tag: 6
} // end struct WithdrawMsg
function decWithdrawMsg(bytes memory raw) internal pure returns (WithdrawMsg memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.chainid = uint64(buf.decVarint());
} else if (tag == 2) {
m.seqnum = uint64(buf.decVarint());
} else if (tag == 3) {
m.receiver = Pb._address(buf.decBytes());
} else if (tag == 4) {
m.token = Pb._address(buf.decBytes());
} else if (tag == 5) {
m.amount = Pb._uint256(buf.decBytes());
} else if (tag == 6) {
m.refid = Pb._bytes32(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder WithdrawMsg
}
================================================
FILE: contracts/libraries/PbSgn.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: contracts/libraries/proto/sgn.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbSgn {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct Withdrawal {
address account; // tag: 1
address token; // tag: 2
uint256 cumulativeAmount; // tag: 3
} // end struct Withdrawal
function decWithdrawal(bytes memory raw) internal pure returns (Withdrawal memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.account = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.token = Pb._address(buf.decBytes());
} else if (tag == 3) {
m.cumulativeAmount = Pb._uint256(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder Withdrawal
}
================================================
FILE: contracts/libraries/PbStaking.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
// Code generated by protoc-gen-sol. DO NOT EDIT.
// source: contracts/libraries/proto/staking.proto
pragma solidity 0.8.17;
import "./Pb.sol";
library PbStaking {
using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj
struct StakingReward {
address recipient; // tag: 1
uint256 cumulativeRewardAmount; // tag: 2
} // end struct StakingReward
function decStakingReward(bytes memory raw) internal pure returns (StakingReward memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.recipient = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.cumulativeRewardAmount = Pb._uint256(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder StakingReward
struct Slash {
address validator; // tag: 1
uint64 nonce; // tag: 2
uint64 slashFactor; // tag: 3
uint64 expireTime; // tag: 4
uint64 jailPeriod; // tag: 5
AcctAmtPair[] collectors; // tag: 6
} // end struct Slash
function decSlash(bytes memory raw) internal pure returns (Slash memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256[] memory cnts = buf.cntTags(6);
m.collectors = new AcctAmtPair[](cnts[6]);
cnts[6] = 0; // reset counter for later use
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.validator = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.nonce = uint64(buf.decVarint());
} else if (tag == 3) {
m.slashFactor = uint64(buf.decVarint());
} else if (tag == 4) {
m.expireTime = uint64(buf.decVarint());
} else if (tag == 5) {
m.jailPeriod = uint64(buf.decVarint());
} else if (tag == 6) {
m.collectors[cnts[6]] = decAcctAmtPair(buf.decBytes());
cnts[6]++;
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder Slash
struct AcctAmtPair {
address account; // tag: 1
uint256 amount; // tag: 2
} // end struct AcctAmtPair
function decAcctAmtPair(bytes memory raw) internal pure returns (AcctAmtPair memory m) {
Pb.Buffer memory buf = Pb.fromBytes(raw);
uint256 tag;
Pb.WireType wire;
while (buf.hasMore()) {
(tag, wire) = buf.decKey();
if (false) {}
// solidity has no switch/case
else if (tag == 1) {
m.account = Pb._address(buf.decBytes());
} else if (tag == 2) {
m.amount = Pb._uint256(buf.decBytes());
} else {
buf.skipValue(wire);
} // skip value of unknown tag
}
} // end decoder AcctAmtPair
}
================================================
FILE: contracts/libraries/Utils.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
library Utils {
// https://ethereum.stackexchange.com/a/83577
// https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/base/Multicall.sol
function getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {
// If the _res length is less than 68, then the transaction failed silently (without a revert message)
if (_returnData.length < 68) return "Transaction reverted silently";
assembly {
// Slice the sighash.
_returnData := add(_returnData, 0x04)
}
return abi.decode(_returnData, (string)); // All that remains is the revert string
}
}
================================================
FILE: contracts/libraries/proto/README.md
================================================
# about soltype
Due to pb gen sol only supports soltype field option defined in the same proto file. When we have multiple proto files that need soltype, it will break on go side. The short term solution is to manually sync proto files to go repo, and remove soltype related.
## proper solution
have a single file like soltype.proto define the field option, then other proto files just import it. pb gen sol need to parse imported proto and ExtName will have soltype.proto package prefix eg. opt.soltype instead of just soltype. unless all protos have same package which will cause issue for pb gen sol as it outputs .sol files based on proto package name.
or maybe we just register the fieldoption w/ proto team officially? still need to import another proto but package will be celer.opt
https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto#L325
# Generating Solidity bindings
From the project repo root, run:
```sh
protoc --sol_out=importpb=true:contracts/libraries contracts/libraries/proto/{filename}.proto
```
================================================
FILE: contracts/libraries/proto/bridge.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package bridge;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
message Relay {
bytes sender = 1 [(soltype) = "address"];
bytes receiver = 2 [(soltype) = "address"];
bytes token = 3 [(soltype) = "address"]; // asset address on dest chain
bytes amount = 4 [(soltype) = "uint256"];
uint64 src_chain_id = 5;
uint64 dst_chain_id = 6;
bytes src_transfer_id = 7 [(soltype) = "bytes32"];
}
================================================
FILE: contracts/libraries/proto/farming.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package farming;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
message FarmingRewards {
// recipient defines the reward recipient
bytes recipient = 1 [(soltype) = "address"];
// token_addresses defines the list of reward token addresses
repeated bytes token_addresses = 2 [(soltype) = "address"];
// cumulative_reward_amounts defines the cumulative amount of rewards
repeated bytes cumulative_reward_amounts = 3 [(soltype) = "uint256"];
}
================================================
FILE: contracts/libraries/proto/pegged.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package pegged;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
// mint pegged token
// triggered by deposit at the original token vault
message Mint {
bytes token = 1 [(soltype) = "address"];
bytes account = 2 [(soltype) = "address"];
bytes amount = 3 [(soltype) = "uint256"];
// depositor defines the account address that made deposit at the original token chain,
// or the account address that burned tokens at another remote chain
// Not applicable to governance-triggered mints.
bytes depositor = 4 [(soltype) = "address"];
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case: the chain ID on which the corresponding deposit or burn happened;
// 2. Governance-triggered mint: the chain ID on which the minting will happen.
uint64 ref_chain_id = 5;
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of deposit/burn-mint: the deposit or burn ID;
// 2. Governance-triggered mint: ID as needed.
bytes ref_id = 6 [(soltype) = "bytes32"];
}
// withdraw locked original tokens
// triggered by burn at the pegged token bridge
message Withdraw {
bytes token = 1 [(soltype) = "address"];
bytes receiver = 2 [(soltype) = "address"];
bytes amount = 3 [(soltype) = "uint256"];
// burn_account defines the account that burned the pegged token.
// Not applicable to fee claims and governance-triggered withdrawals.
bytes burn_account = 4 [(soltype) = "address"];
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case of burn-withdraw: the chain ID on which the corresponding burn happened;
// 2. Pegbridge fee claim: zero / Not applicable;
// 3. Other governance-triggered withdrawals: the chain ID on which the withdrawal will happen.
uint64 ref_chain_id = 5;
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of burn-withdraw: the burn ID;
// 2. Pegbridge fee claim: a per-account nonce;
// 3. Refund for wrong deposit: the deposit ID;
// 4. Governance-triggered withdrawal: ID as needed.
bytes ref_id = 6 [(soltype) = "bytes32"];
}
================================================
FILE: contracts/libraries/proto/pool.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package pool;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
message WithdrawMsg {
uint64 chainid = 1;
uint64 seqnum = 2; // global unique across chains
bytes receiver = 3 [(soltype) = "address"];
bytes token = 4 [(soltype) = "address"];
bytes amount = 5 [(soltype) = "uint256"];
// reference id
// 1. if refund, set as xfer_id
// 2. if claim fee, set as 0x1
// 3. if LP withdraw liquidity, set as 0x0
bytes refid = 6 [(soltype) = "bytes32"];
}
================================================
FILE: contracts/libraries/proto/sgn.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package sgn;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
message Withdrawal {
bytes account = 1 [(soltype) = "address"];
bytes token = 2 [(soltype) = "address"];
bytes cumulative_amount = 3 [(soltype) = "uint256"];
}
================================================
FILE: contracts/libraries/proto/staking.proto
================================================
syntax = "proto3";
option go_package = "github.com/celer-network/sgn-v2/proto/eth";
import "google/protobuf/descriptor.proto";
package staking;
extend google.protobuf.FieldOptions {
string soltype = 1004;
}
message StakingReward {
bytes recipient = 1 [(soltype) = "address"];
bytes cumulative_reward_amount = 2 [(soltype) = "uint256"];
}
message Slash {
bytes validator = 1 [(soltype) = "address"];
uint64 nonce = 2;
uint64 slash_factor = 3;
uint64 expire_time = 4; // block timestamp
uint64 jail_period = 5; // block number
repeated AcctAmtPair collectors = 6;
}
message AcctAmtPair {
bytes account = 1 [(soltype) = "address"];
bytes amount = 2 [(soltype) = "uint256"];
}
================================================
FILE: contracts/liquidity-bridge/Bridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/PbBridge.sol";
import "./Pool.sol";
/**
* @title The liquidity-pool based bridge.
*/
contract Bridge is Pool {
using SafeERC20 for IERC20;
// liquidity events
event Send(
bytes32 transferId,
address sender,
address receiver,
address token,
uint256 amount,
uint64 dstChainId,
uint64 nonce,
uint32 maxSlippage
);
event Relay(
bytes32 transferId,
address sender,
address receiver,
address token,
uint256 amount,
uint64 srcChainId,
bytes32 srcTransferId
);
// gov events
event MinSendUpdated(address token, uint256 amount);
event MaxSendUpdated(address token, uint256 amount);
mapping(bytes32 => bool) public transfers;
mapping(address => uint256) public minSend; // send _amount must > minSend
mapping(address => uint256) public maxSend;
// min allowed max slippage uint32 value is slippage * 1M, eg. 0.5% -> 5000
uint32 public minimalMaxSlippage;
/**
* @notice Send a cross-chain transfer via the liquidity pool-based bridge.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _receiver The address of the receiver.
* @param _token The address of the token.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded.
*/
function send(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage // slippage * 1M, eg. 0.5% -> 5000
) external nonReentrant whenNotPaused {
bytes32 transferId = _send(_receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage);
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit Send(transferId, msg.sender, _receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage);
}
/**
* @notice Send a cross-chain transfer via the liquidity pool-based bridge using the native token.
* @param _receiver The address of the receiver.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A unique number. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded.
*/
function sendNative(
address _receiver,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage
) external payable nonReentrant whenNotPaused {
require(msg.value == _amount, "Amount mismatch");
require(nativeWrap != address(0), "Native wrap not set");
bytes32 transferId = _send(_receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage);
IWETH(nativeWrap).deposit{value: _amount}();
emit Send(transferId, msg.sender, _receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage);
}
function _send(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage
) private returns (bytes32) {
require(_amount > minSend[_token], "amount too small");
require(maxSend[_token] == 0 || _amount <= maxSend[_token], "amount too large");
require(_maxSlippage > minimalMaxSlippage, "max slippage too small");
bytes32 transferId = keccak256(
// uint64(block.chainid) for consistency as entire system uses uint64 for chain id
// len = 20 + 20 + 20 + 32 + 8 + 8 + 8 = 116
abi.encodePacked(msg.sender, _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid))
);
require(transfers[transferId] == false, "transfer exists");
transfers[transferId] = true;
return transferId;
}
/**
* @notice Relay a cross-chain transfer sent from a liquidity pool-based bridge on another chain.
* @param _relayRequest The serialized Relay protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function relay(
bytes calldata _relayRequest,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Relay"));
verifySigs(abi.encodePacked(domain, _relayRequest), _sigs, _signers, _powers);
PbBridge.Relay memory request = PbBridge.decRelay(_relayRequest);
// len = 20 + 20 + 20 + 32 + 8 + 8 + 32 = 140
bytes32 transferId = keccak256(
abi.encodePacked(
request.sender,
request.receiver,
request.token,
request.amount,
request.srcChainId,
request.dstChainId,
request.srcTransferId
)
);
require(transfers[transferId] == false, "transfer exists");
transfers[transferId] = true;
_updateVolume(request.token, request.amount);
uint256 delayThreshold = delayThresholds[request.token];
if (delayThreshold > 0 && request.amount > delayThreshold) {
_addDelayedTransfer(transferId, request.receiver, request.token, request.amount);
} else {
_sendToken(request.receiver, request.token, request.amount);
}
emit Relay(
transferId,
request.sender,
request.receiver,
request.token,
request.amount,
request.srcChainId,
request.srcTransferId
);
}
function setMinSend(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minSend[_tokens[i]] = _amounts[i];
emit MinSendUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxSend(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxSend[_tokens[i]] = _amounts[i];
emit MaxSendUpdated(_tokens[i], _amounts[i]);
}
}
function setMinimalMaxSlippage(uint32 _minimalMaxSlippage) external onlyGovernor {
minimalMaxSlippage = _minimalMaxSlippage;
}
// This is needed to receive ETH when calling `IWETH.withdraw`
receive() external payable {}
}
================================================
FILE: contracts/liquidity-bridge/FarmingRewards.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/ISigsVerifier.sol";
import "../libraries/PbFarming.sol";
import "../safeguard/Pauser.sol";
/**
* @title A contract to hold and distribute farming rewards.
*/
contract FarmingRewards is Pauser {
using SafeERC20 for IERC20;
ISigsVerifier public immutable sigsVerifier;
// recipient => tokenAddress => amount
mapping(address => mapping(address => uint256)) public claimedRewardAmounts;
event FarmingRewardClaimed(address indexed recipient, address indexed token, uint256 reward);
event FarmingRewardContributed(address indexed contributor, address indexed token, uint256 contribution);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Claim rewards
* @dev Here we use cumulative reward to make claim process idempotent
* @param _rewardsRequest rewards request bytes coded in protobuf
* @param _sigs list of signatures sorted by signer addresses in ascending order
* @param _signers sorted list of current signers
* @param _powers powers of current signers
*/
function claimRewards(
bytes calldata _rewardsRequest,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "FarmingRewards"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _rewardsRequest), _sigs, _signers, _powers);
PbFarming.FarmingRewards memory rewards = PbFarming.decFarmingRewards(_rewardsRequest);
bool hasNewReward;
for (uint256 i = 0; i < rewards.tokenAddresses.length; i++) {
address token = rewards.tokenAddresses[i];
uint256 cumulativeRewardAmount = rewards.cumulativeRewardAmounts[i];
uint256 newReward = cumulativeRewardAmount - claimedRewardAmounts[rewards.recipient][token];
if (newReward > 0) {
hasNewReward = true;
claimedRewardAmounts[rewards.recipient][token] = cumulativeRewardAmount;
IERC20(token).safeTransfer(rewards.recipient, newReward);
emit FarmingRewardClaimed(rewards.recipient, token, newReward);
}
}
require(hasNewReward, "No new reward");
}
/**
* @notice Contribute reward tokens to the reward pool
* @param _token the address of the token to contribute
* @param _amount the amount of the token to contribute
*/
function contributeToRewardPool(address _token, uint256 _amount) external whenNotPaused {
address contributor = msg.sender;
IERC20(_token).safeTransferFrom(contributor, address(this), _amount);
emit FarmingRewardContributed(contributor, _token, _amount);
}
/**
* @notice Owner drains tokens when the contract is paused
* @dev emergency use only
* @param _token the address of the token to drain
* @param _amount drained token amount
*/
function drainToken(address _token, uint256 _amount) external whenPaused onlyOwner {
IERC20(_token).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/liquidity-bridge/Pool.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/IWETH.sol";
import "../libraries/PbPool.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
import "./Signers.sol";
/**
* @title Liquidity pool functions for {Bridge}.
*/
contract Pool is Signers, ReentrancyGuard, Pauser, VolumeControl, DelayedTransfer {
using SafeERC20 for IERC20;
uint64 public addseq; // ensure unique LiquidityAdded event, start from 1
mapping(address => uint256) public minAdd; // add _amount must > minAdd
// map of successful withdraws, if true means already withdrew money or added to delayedTransfers
mapping(bytes32 => bool) public withdraws;
// erc20 wrap of gas token of this chain, eg. WETH, when relay ie. pay out,
// if request.token equals this, will withdraw and send native token to receiver
// note we don't check whether it's zero address. when this isn't set, and request.token
// is all 0 address, guarantee fail
address public nativeWrap;
// when transfer native token after wrap, use this gas used config.
uint256 public nativeTokenTransferGas = 50000;
// liquidity events
event LiquidityAdded(
uint64 seqnum,
address provider,
address token,
uint256 amount // how many tokens were added
);
event WithdrawDone(
bytes32 withdrawId,
uint64 seqnum,
address receiver,
address token,
uint256 amount,
bytes32 refid
);
event MinAddUpdated(address token, uint256 amount);
/**
* @notice Add liquidity to the pool-based bridge.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The address of the token.
* @param _amount The amount to add.
*/
function addLiquidity(address _token, uint256 _amount) external nonReentrant whenNotPaused {
require(_amount > minAdd[_token], "amount too small");
addseq += 1;
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit LiquidityAdded(addseq, msg.sender, _token, _amount);
}
/**
* @notice Add native token liquidity to the pool-based bridge.
* @param _amount The amount to add.
*/
function addNativeLiquidity(uint256 _amount) external payable nonReentrant whenNotPaused {
require(msg.value == _amount, "Amount mismatch");
require(nativeWrap != address(0), "Native wrap not set");
require(_amount > minAdd[nativeWrap], "amount too small");
addseq += 1;
IWETH(nativeWrap).deposit{value: _amount}();
emit LiquidityAdded(addseq, msg.sender, nativeWrap, _amount);
}
/**
* @notice Withdraw funds from the bridge pool.
* @param _wdmsg The serialized Withdraw protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A withdrawal must be
* signed-off by +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdraw(
bytes calldata _wdmsg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "WithdrawMsg"));
verifySigs(abi.encodePacked(domain, _wdmsg), _sigs, _signers, _powers);
// decode and check wdmsg
PbPool.WithdrawMsg memory wdmsg = PbPool.decWithdrawMsg(_wdmsg);
// len = 8 + 8 + 20 + 20 + 32 = 88
bytes32 wdId = keccak256(
abi.encodePacked(wdmsg.chainid, wdmsg.seqnum, wdmsg.receiver, wdmsg.token, wdmsg.amount)
);
require(withdraws[wdId] == false, "withdraw already succeeded");
withdraws[wdId] = true;
_updateVolume(wdmsg.token, wdmsg.amount);
uint256 delayThreshold = delayThresholds[wdmsg.token];
if (delayThreshold > 0 && wdmsg.amount > delayThreshold) {
_addDelayedTransfer(wdId, wdmsg.receiver, wdmsg.token, wdmsg.amount);
} else {
_sendToken(wdmsg.receiver, wdmsg.token, wdmsg.amount);
}
emit WithdrawDone(wdId, wdmsg.seqnum, wdmsg.receiver, wdmsg.token, wdmsg.amount, wdmsg.refid);
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
_sendToken(transfer.receiver, transfer.token, transfer.amount);
}
function setMinAdd(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minAdd[_tokens[i]] = _amounts[i];
emit MinAddUpdated(_tokens[i], _amounts[i]);
}
}
function _sendToken(
address _receiver,
address _token,
uint256 _amount
) internal {
if (_token == nativeWrap) {
// withdraw then transfer native to receiver
IWETH(nativeWrap).withdraw(_amount);
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
// set nativeWrap, for relay requests, if token == nativeWrap, will withdraw first then transfer native to receiver
function setWrap(address _weth) external onlyOwner {
nativeWrap = _weth;
}
// setNativeTransferGasUsed, native transfer will use this config.
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyGovernor {
nativeTokenTransferGas = _gasUsed;
}
}
================================================
FILE: contracts/liquidity-bridge/Signers.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../safeguard/Ownable.sol";
import "../interfaces/ISigsVerifier.sol";
/**
* @title Multi-sig verification and management functions for {Bridge}.
*/
contract Signers is Ownable, ISigsVerifier {
using ECDSA for bytes32;
bytes32 public ssHash;
uint256 public triggerTime; // timestamp when last update was triggered
// reset can be called by the owner address for emergency recovery
uint256 public resetTime;
uint256 public noticePeriod; // advance notice period as seconds for reset
uint256 constant MAX_INT = 2 ** 256 - 1;
event SignersUpdated(address[] _signers, uint256[] _powers);
event ResetNotification(uint256 resetTime);
/**
* @notice Verifies that a message is signed by a quorum among the signers
* The sigs must be sorted by signer addresses in ascending order.
* @param _msg signed message
* @param _sigs list of signatures sorted by signer addresses in ascending order
* @param _signers sorted list of current signers
* @param _powers powers of current signers
*/
function verifySigs(
bytes memory _msg,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) public view override {
bytes32 h = keccak256(abi.encodePacked(_signers, _powers));
require(ssHash == h, "Mismatch current signers");
_verifySignedPowers(keccak256(_msg).toEthSignedMessageHash(), _sigs, _signers, _powers);
}
/**
* @notice Update new signers.
* @param _newSigners sorted list of new signers
* @param _curPowers powers of new signers
* @param _sigs list of signatures sorted by signer addresses in ascending order
* @param _curSigners sorted list of current signers
* @param _curPowers powers of current signers
*/
function updateSigners(
uint256 _triggerTime,
address[] calldata _newSigners,
uint256[] calldata _newPowers,
bytes[] calldata _sigs,
address[] calldata _curSigners,
uint256[] calldata _curPowers
) external {
// use trigger time for nonce protection, must be ascending
require(_triggerTime > triggerTime, "Trigger time is not increasing");
// make sure triggerTime is not too large, as it cannot be decreased once set
require(_triggerTime < block.timestamp + 3600, "Trigger time is too large");
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "UpdateSigners"));
verifySigs(abi.encodePacked(domain, _triggerTime, _newSigners, _newPowers), _sigs, _curSigners, _curPowers);
_updateSigners(_newSigners, _newPowers);
triggerTime = _triggerTime;
}
/**
* @notice reset signers, only used for init setup and emergency recovery
*/
function resetSigners(address[] calldata _signers, uint256[] calldata _powers) external onlyOwner {
require(block.timestamp > resetTime, "not reach reset time");
resetTime = MAX_INT;
_updateSigners(_signers, _powers);
}
function notifyResetSigners() external onlyOwner {
resetTime = block.timestamp + noticePeriod;
emit ResetNotification(resetTime);
}
function increaseNoticePeriod(uint256 period) external onlyOwner {
require(period > noticePeriod, "notice period can only be increased");
noticePeriod = period;
}
// separate from verifySigs func to avoid "stack too deep" issue
function _verifySignedPowers(
bytes32 _hash,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) private pure {
require(_signers.length == _powers.length, "signers and powers length not match");
uint256 totalPower; // sum of all signer.power
for (uint256 i = 0; i < _signers.length; i++) {
totalPower += _powers[i];
}
uint256 quorum = (totalPower * 2) / 3 + 1;
uint256 signedPower; // sum of signer powers who are in sigs
address prev = address(0);
uint256 index = 0;
for (uint256 i = 0; i < _sigs.length; i++) {
address signer = _hash.recover(_sigs[i]);
require(signer > prev, "signers not in ascending order");
prev = signer;
// now find match signer add its power
while (signer > _signers[index]) {
index += 1;
require(index < _signers.length, "signer not found");
}
if (signer == _signers[index]) {
signedPower += _powers[index];
}
if (signedPower >= quorum) {
// return early to save gas
return;
}
}
revert("quorum not reached");
}
function _updateSigners(address[] calldata _signers, uint256[] calldata _powers) private {
require(_signers.length == _powers.length, "signers and powers length not match");
address prev = address(0);
for (uint256 i = 0; i < _signers.length; i++) {
require(_signers[i] > prev, "New signers not in ascending order");
prev = _signers[i];
}
ssHash = keccak256(abi.encodePacked(_signers, _powers));
emit SignersUpdated(_signers, _powers);
}
}
================================================
FILE: contracts/liquidity-bridge/WithdrawInbox.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../safeguard/Ownable.sol";
/**
* @title A contract to initiate withdrawal requests for contracts that provide liquidity to {Bridge}.
*/
contract WithdrawInbox is Ownable {
// min allowed max slippage uint32 value is slippage * 1M, eg. 0.5% -> 5000
uint32 public minimalMaxSlippage;
// the period of time during which a withdrawal request is intended to be valid
uint256 public validityPeriod;
// contract LP withdrawal request
event WithdrawalRequest(
uint64 seqNum,
address sender,
address receiver,
uint64 toChain,
uint64[] fromChains,
address[] tokens,
uint32[] ratios,
uint32[] slippages,
uint256 deadline
);
constructor() {
// default validityPeriod is 2 hours
validityPeriod = 7200;
}
/**
* @notice Withdraw liquidity from the pool-based bridge.
* NOTE: Each of your withdrawal request should have different _wdSeq.
* NOTE: Tokens to withdraw within one withdrawal request should have the same symbol.
* @param _wdSeq The unique sequence number to identify this withdrawal request.
* @param _receiver The receiver address on _toChain.
* @param _toChain The chain Id to receive the withdrawn tokens.
* @param _fromChains The chain Ids to withdraw tokens.
* @param _tokens The token to withdraw on each fromChain.
* @param _ratios The withdrawal ratios of each token.
* @param _slippages The max slippages of each token for cross-chain withdraw.
*/
function withdraw(
uint64 _wdSeq,
address _receiver,
uint64 _toChain,
uint64[] calldata _fromChains,
address[] calldata _tokens,
uint32[] calldata _ratios,
uint32[] calldata _slippages
) external {
require(_fromChains.length > 0, "empty withdrawal request");
require(
_tokens.length == _fromChains.length &&
_ratios.length == _fromChains.length &&
_slippages.length == _fromChains.length,
"length mismatch"
);
for (uint256 i = 0; i < _ratios.length; i++) {
require(_ratios[i] > 0 && _ratios[i] <= 1e8, "invalid ratio");
require(_slippages[i] >= minimalMaxSlippage, "slippage too small");
}
uint256 _deadline = block.timestamp + validityPeriod;
emit WithdrawalRequest(
_wdSeq,
msg.sender,
_receiver,
_toChain,
_fromChains,
_tokens,
_ratios,
_slippages,
_deadline
);
}
// ------------------------Admin operations--------------------------
function setMinimalMaxSlippage(uint32 _minimalMaxSlippage) external onlyOwner {
minimalMaxSlippage = _minimalMaxSlippage;
}
function setValidityPeriod(uint256 _validityPeriod) external onlyOwner {
validityPeriod = _validityPeriod;
}
}
================================================
FILE: contracts/message/CHANGELOG.md
================================================
# 0.2.0 (2022-03-28)
## MsgDataTypes
- Most message bridge related data types are now in a separate library `MsgDataTypes`. Some types have their naming changed but there is no structural change.
- `MsgDataTypes.BridgeSendType` (originally `BridgeType`) now has three more types `PegV2Deposit`, `PegV2Burn`, and `PegV2BurnFrom`.
## MessageReceiverApp
- Receiver functions `executeMessageWithTransfer`, `executeMessageWithTransferFallback`, `executeMessageWithTransferRefund`, `executeMessage` now has an extra param `address _executor` that the app developer could use to check who submitted the execution.
- Receiver functions' required return value also changed from a `boolean success` to `ExecutionStatus` to accommodate a third status `ExecutionStatus.Retry`. This status indicates that the message processing should not be regarded as "processed" by MessageBus and the processing should simply be ignored. This status can be used in conjunction with the aforementioned `address _executor` to completely ignore executions that are not originated from a specific executor.
## MessageBus (MessageBusReceiver)
- Added a helper function `refund()` to aggregate refund call to `Bridge.withdraw()` and refund functions in other bridges and `MessageBus.executeMessageWithTransferRefund()` into one call.
- Added a new event `NeedRetry`, emitted when the execution logic in an app contract returns `ExecutionStatus.Retry`.
- Added a field `srcTxHash` in `Executed` and `NeedRetry` event to enable third parties to co-verify a whether a transfer/message send does happen on the source chain (in the name of not completely trusting message bus).
================================================
FILE: contracts/message/README.md
================================================
[Celer Inter-chain Message (IM) Developer Docs](https://im-docs.celer.network/)
================================================
FILE: contracts/message/apps/RFQ.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../framework/MessageSenderApp.sol";
import "../framework/MessageReceiverApp.sol";
import "../../safeguard/Pauser.sol";
import "../../safeguard/Governor.sol";
import "../../message/interfaces/IMessageBus.sol";
import "../../interfaces/IWETH.sol";
/** @title rfq contract */
contract RFQ is MessageSenderApp, MessageReceiverApp, Pauser, Governor {
using SafeERC20 for IERC20;
using ECDSA for bytes32;
struct Quote {
uint64 srcChainId;
address srcToken;
uint256 srcAmount;
uint256 srcReleaseAmount;
uint64 dstChainId;
address dstToken;
uint256 dstAmount;
uint64 deadline;
uint64 nonce;
address sender;
address receiver;
address refundTo;
address liquidityProvider;
}
enum QuoteStatus {
Null,
SrcDeposited, // sender deposited
SrcReleased, // released ERC20 token to liquidity provider
SrcReleasedNative, // released native token to liquidity provider
SrcRefunded, // refunded ERC20 token to refundTo/sender
SrcRefundedNative, // refunded native token to refundTo/sender
DstRefundInitiated, // refund initiated
DstTransferred, // transferred ERC20 token to receiver
DstTransferredNative // transferred native token to receiver
}
enum MessageType {
Null,
Release,
Refund
}
address public nativeWrap;
uint256 public nativeTokenTransferGas = 50000;
mapping(uint64 => address) public remoteRfqContracts;
// msg => bool
mapping(bytes32 => bool) public unconsumedMsg;
// quoteHash => QuoteStatus
mapping(bytes32 => QuoteStatus) public quotes;
address public treasuryAddr;
uint32 public feePercGlobal;
// chainId => feePercOverride, support override fee perc of this chain
mapping(uint64 => uint32) public feePercOverride;
// tokenAddr => feeBalance
mapping(address => uint256) public protocolFee;
// market maker => allowed signer
mapping(address => address) public allowedSigner;
event SrcDeposited(bytes32 quoteHash, Quote quote);
event DstTransferred(bytes32 quoteHash, address receiver, address dstToken, uint256 amount);
event RefundInitiated(bytes32 quoteHash);
event SrcReleased(bytes32 quoteHash, address liquidityProvider, address srcToken, uint256 amount);
event Refunded(bytes32 quoteHash, address refundTo, address srcToken, uint256 amount);
event RfqContractsUpdated(uint64[] chainIds, address[] remoteRfqContracts);
event FeePercUpdated(uint64[] chainIds, uint32[] feePercs);
event TreasuryAddrUpdated(address treasuryAddr);
event FeeCollected(address treasuryAddr, address token, uint256 amount);
constructor(address _messageBus) {
messageBus = _messageBus;
}
function srcDeposit(Quote calldata _quote, uint64 _submissionDeadline)
external
payable
whenNotPaused
returns (bytes32)
{
bytes32 quoteHash = _srcDeposit(_quote, _submissionDeadline, msg.value);
IERC20(_quote.srcToken).safeTransferFrom(msg.sender, address(this), _quote.srcAmount);
return quoteHash;
}
function srcDepositNative(Quote calldata _quote, uint64 _submissionDeadline)
external
payable
whenNotPaused
returns (bytes32)
{
require(nativeWrap != address(0), "Rfq: native wrap not set");
require(_quote.srcToken == nativeWrap, "Rfq: src token mismatch");
require(msg.value >= _quote.srcAmount, "Rfq: insufficient amount");
bytes32 quoteHash = _srcDeposit(_quote, _submissionDeadline, msg.value - _quote.srcAmount);
IWETH(nativeWrap).deposit{value: _quote.srcAmount}();
return quoteHash;
}
function _srcDeposit(
Quote calldata _quote,
uint64 _submissionDeadline,
uint256 _msgFee
) private returns (bytes32) {
require(
_submissionDeadline > block.timestamp && _quote.deadline > _submissionDeadline,
"Rfq: inappropriate deadline"
);
require(
_quote.receiver != address(0) && _quote.liquidityProvider != address(0),
"Rfq: invalid receiver or liquidityProvider"
);
require(_quote.srcChainId == uint64(block.chainid), "Rfq: src chainId mismatch");
require(_quote.sender == msg.sender, "Rfq: sender mismatch");
bytes32 quoteHash = getQuoteHash(_quote);
require(quotes[quoteHash] == QuoteStatus.Null, "Rfq: quote hash exists");
uint256 rfqFee = getRfqFee(_quote.dstChainId, _quote.srcAmount);
require(rfqFee <= _quote.srcAmount - _quote.srcReleaseAmount, "Rfq: insufficient protocol fee");
quotes[quoteHash] = QuoteStatus.SrcDeposited;
if (_quote.srcChainId != _quote.dstChainId) {
address msgReceiver = remoteRfqContracts[_quote.dstChainId];
require(msgReceiver != address(0), "Rfq: dst contract not set");
bytes memory message = abi.encodePacked(quoteHash);
sendMessage(msgReceiver, _quote.dstChainId, message, _msgFee);
}
emit SrcDeposited(quoteHash, _quote);
return quoteHash;
}
function dstTransfer(Quote calldata _quote) external payable whenNotPaused {
(bytes32 quoteHash, address msgReceiver) = _dstTransferCheck(_quote);
quotes[quoteHash] = QuoteStatus.DstTransferred;
bytes memory message = abi.encodePacked(keccak256(abi.encodePacked(quoteHash, MessageType.Release)));
sendMessage(msgReceiver, _quote.srcChainId, message, msg.value);
IERC20(_quote.dstToken).safeTransferFrom(msg.sender, _quote.receiver, _quote.dstAmount);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
function dstTransferNative(Quote calldata _quote) external payable whenNotPaused {
require(_quote.dstToken == nativeWrap, "Rfq: dst token mismatch");
require(msg.value >= _quote.dstAmount, "Rfq: insufficient amount");
(bytes32 quoteHash, address msgReceiver) = _dstTransferCheck(_quote);
quotes[quoteHash] = QuoteStatus.DstTransferredNative;
bytes memory message = abi.encodePacked(keccak256(abi.encodePacked(quoteHash, MessageType.Release)));
sendMessage(msgReceiver, _quote.srcChainId, message, msg.value - _quote.dstAmount);
_transferNativeToken(_quote.receiver, _quote.dstAmount);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
// As transferFrom is not available for native token, dstTransferNativeWithSig is not supported
function dstTransferWithSig(Quote calldata _quote, bytes calldata _sig) external payable whenNotPaused {
(bytes32 quoteHash, address msgReceiver) = _dstTransferCheck(_quote);
verifySigOfQuoteHash(_quote.liquidityProvider, quoteHash, _sig);
quotes[quoteHash] = QuoteStatus.DstTransferred;
bytes memory message = abi.encodePacked(keccak256(abi.encodePacked(quoteHash, MessageType.Release)));
sendMessage(msgReceiver, _quote.srcChainId, message, msg.value);
IERC20(_quote.dstToken).safeTransferFrom(_quote.liquidityProvider, _quote.receiver, _quote.dstAmount);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
function sameChainTransfer(Quote calldata _quote, bool _releaseNative) external payable whenNotPaused {
require(_quote.srcChainId == _quote.dstChainId, "Rfq: not same chain swap");
(bytes32 quoteHash, ) = _dstTransferCheck(_quote);
IERC20(_quote.dstToken).safeTransferFrom(msg.sender, _quote.receiver, _quote.dstAmount);
_srcRelease(_quote, quoteHash, _releaseNative);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
function sameChainTransferNative(Quote calldata _quote, bool _releaseNative) external payable whenNotPaused {
require(_quote.srcChainId == _quote.dstChainId, "Rfq: not same chain swap");
require(_quote.dstToken == nativeWrap, "Rfq: dst token mismatch");
require(msg.value == _quote.dstAmount, "Rfq: native token amount mismatch");
(bytes32 quoteHash, ) = _dstTransferCheck(_quote);
_transferNativeToken(_quote.receiver, _quote.dstAmount);
_srcRelease(_quote, quoteHash, _releaseNative);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
// As transferFrom is not available for native token, sameChainTransferNativeWithSig is not supported
function sameChainTransferWithSig(
Quote calldata _quote,
bool _releaseNative,
bytes calldata _sig
) external payable whenNotPaused {
require(_quote.srcChainId == _quote.dstChainId, "Rfq: not same chain swap");
(bytes32 quoteHash, ) = _dstTransferCheck(_quote);
verifySigOfQuoteHash(_quote.liquidityProvider, quoteHash, _sig);
IERC20(_quote.dstToken).safeTransferFrom(_quote.liquidityProvider, _quote.receiver, _quote.dstAmount);
_srcRelease(_quote, quoteHash, _releaseNative);
emit DstTransferred(quoteHash, _quote.receiver, _quote.dstToken, _quote.dstAmount);
}
function _dstTransferCheck(Quote calldata _quote) private view returns (bytes32, address) {
require(_quote.deadline > block.timestamp, "Rfq: transfer deadline passed");
require(_quote.dstChainId == uint64(block.chainid), "Rfq: dst chainId mismatch");
bytes32 quoteHash = getQuoteHash(_quote);
address msgReceiver = remoteRfqContracts[_quote.srcChainId];
if (_quote.srcChainId != _quote.dstChainId) {
require(quotes[quoteHash] == QuoteStatus.Null, "Rfq: quote already executed");
require(msgReceiver != address(0), "Rfq: dst rfq contract not set");
} else {
require(quotes[quoteHash] == QuoteStatus.SrcDeposited, "Rfq: no deposit on same chain");
}
return (quoteHash, msgReceiver);
}
function srcRelease(Quote calldata _quote, bytes calldata _execMsgCallData) external whenNotPaused {
bytes32 quoteHash = _srcReleaseCheck(_quote, _execMsgCallData);
_srcRelease(_quote, quoteHash, false);
}
function srcReleaseNative(Quote calldata _quote, bytes calldata _execMsgCallData) external whenNotPaused {
require(_quote.srcToken == nativeWrap, "Rfq: src token mismatch");
bytes32 quoteHash = _srcReleaseCheck(_quote, _execMsgCallData);
_srcRelease(_quote, quoteHash, true);
}
function _srcReleaseCheck(Quote calldata _quote, bytes calldata _execMsgCallData) private returns (bytes32) {
bytes32 quoteHash = getQuoteHash(_quote);
require(quotes[quoteHash] == QuoteStatus.SrcDeposited, "Rfq: incorrect quote hash");
_receiveMessage(_execMsgCallData, quoteHash, MessageType.Release);
return quoteHash;
}
function _srcRelease(
Quote calldata _quote,
bytes32 _quoteHash,
bool _releaseNative
) private {
protocolFee[_quote.srcToken] += (_quote.srcAmount - _quote.srcReleaseAmount);
if (_releaseNative) {
quotes[_quoteHash] = QuoteStatus.SrcReleasedNative;
_withdrawNativeToken(_quote.liquidityProvider, _quote.srcReleaseAmount);
} else {
quotes[_quoteHash] = QuoteStatus.SrcReleased;
IERC20(_quote.srcToken).safeTransfer(_quote.liquidityProvider, _quote.srcReleaseAmount);
}
emit SrcReleased(_quoteHash, _quote.liquidityProvider, _quote.srcToken, _quote.srcReleaseAmount);
}
function requestRefund(Quote calldata _quote) external payable whenNotPaused {
require(_quote.deadline < block.timestamp, "Rfq: transfer deadline not passed");
require(_quote.dstChainId == uint64(block.chainid), "Rfq: dst chainId mismatch");
address _receiver = remoteRfqContracts[_quote.srcChainId];
require(_receiver != address(0), "Rfq: src rfq contract not set");
bytes32 quoteHash = getQuoteHash(_quote);
require(quotes[quoteHash] == QuoteStatus.Null, "Rfq: quote already executed");
quotes[quoteHash] = QuoteStatus.DstRefundInitiated;
bytes memory message = abi.encodePacked(keccak256(abi.encodePacked(quoteHash, MessageType.Refund)));
sendMessage(_receiver, _quote.srcChainId, message, msg.value);
emit RefundInitiated(quoteHash);
}
function executeRefund(Quote calldata _quote, bytes calldata _execMsgCallData) external whenNotPaused {
(bytes32 quoteHash, address receiver) = _executeRefund(_quote, _execMsgCallData);
quotes[quoteHash] = QuoteStatus.SrcRefunded;
IERC20(_quote.srcToken).safeTransfer(receiver, _quote.srcAmount);
emit Refunded(quoteHash, receiver, _quote.srcToken, _quote.srcAmount);
}
function executeRefundNative(Quote calldata _quote, bytes calldata _execMsgCallData) external whenNotPaused {
require(_quote.srcToken == nativeWrap, "Rfq: src token mismatch");
(bytes32 quoteHash, address receiver) = _executeRefund(_quote, _execMsgCallData);
quotes[quoteHash] = QuoteStatus.SrcRefundedNative;
_withdrawNativeToken(receiver, _quote.srcAmount);
emit Refunded(quoteHash, receiver, _quote.srcToken, _quote.srcAmount);
}
function _executeRefund(Quote calldata _quote, bytes calldata _execMsgCallData) private returns (bytes32, address) {
bytes32 quoteHash = getQuoteHash(_quote);
require(quotes[quoteHash] == QuoteStatus.SrcDeposited, "Rfq: incorrect quote hash");
if (_quote.srcChainId != _quote.dstChainId) {
_receiveMessage(_execMsgCallData, quoteHash, MessageType.Refund);
} else {
require(_quote.deadline < block.timestamp, "Rfq: transfer deadline not passed");
}
address receiver = (_quote.refundTo == address(0)) ? _quote.sender : _quote.refundTo;
return (quoteHash, receiver);
}
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
require(_message.length == 32, "Rfq: incorrect message length");
address expectedSender = remoteRfqContracts[_srcChainId];
if (expectedSender != _sender) {
return ExecutionStatus.Retry;
}
unconsumedMsg[bytes32(_message)] = true;
return ExecutionStatus.Success;
}
function collectFee(address _token) external {
require(treasuryAddr != address(0), "Rfq: treasury address not set");
uint256 feeAmount = protocolFee[_token];
protocolFee[_token] = 0;
IERC20(_token).safeTransfer(treasuryAddr, feeAmount);
emit FeeCollected(treasuryAddr, _token, feeAmount);
}
function registerAllowedSigner(address _signer) external {
if (_signer == address(0)) {
delete (allowedSigner[msg.sender]);
} else {
allowedSigner[msg.sender] = _signer;
}
}
// This is needed to receive ETH
receive() external payable {}
//=========================== helper functions ==========================
function getQuoteHash(Quote calldata _quote) public pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
_quote.srcChainId,
_quote.srcToken,
_quote.srcAmount,
_quote.srcReleaseAmount,
_quote.dstChainId,
_quote.dstToken,
_quote.dstAmount,
_quote.deadline,
_quote.nonce,
_quote.sender,
_quote.receiver,
_quote.refundTo,
_quote.liquidityProvider
)
);
}
function getRfqFee(uint64 _chainId, uint256 _amount) public view returns (uint256) {
uint32 feePerc = feePercOverride[_chainId];
if (feePerc == 0) {
feePerc = feePercGlobal;
}
return (_amount * feePerc) / 1e6;
}
function getMsgFee(bytes calldata _message) public view returns (uint256) {
return IMessageBus(messageBus).calcFee(_message);
}
function getSignerOfQuoteHash(bytes32 _quoteHash, bytes calldata _sig) public view returns (address) {
bytes32 msgHash = keccak256(abi.encodePacked(block.chainid, address(this), "AllowedTransfer", _quoteHash))
.toEthSignedMessageHash();
return msgHash.recover(_sig);
}
function verifySigOfQuoteHash(
address _liquidityProvider,
bytes32 _quoteHash,
bytes calldata _sig
) public view {
address signer = getSignerOfQuoteHash(_quoteHash, _sig);
require(
signer == _liquidityProvider ||
(allowedSigner[_liquidityProvider] != address(0) && signer == allowedSigner[_liquidityProvider]),
"Rfq: not allowed signer"
);
}
function _receiveMessage(
bytes calldata _execMsgCallData,
bytes32 _quoteHash,
MessageType _msgType
) private {
bytes32 expectedMsg = keccak256(abi.encodePacked(_quoteHash, _msgType));
if (!unconsumedMsg[expectedMsg]) {
(bool success, ) = messageBus.call(_execMsgCallData);
require(success, "execute msg failed");
}
require(unconsumedMsg[expectedMsg], "Rfq: invalid msg");
delete unconsumedMsg[expectedMsg];
}
function _transferNativeToken(address _receiver, uint256 _amount) private {
require(nativeWrap != address(0), "Rfq: native wrap not set");
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "Rfq: failed to transfer native token");
}
function _withdrawNativeToken(address _receiver, uint256 _amount) private {
require(nativeWrap != address(0), "Rfq: native wrap not set");
IWETH(nativeWrap).withdraw(_amount);
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "Rfq: failed to withdraw native token");
}
//=========================== admin operations ==========================
function setRemoteRfqContracts(uint64[] calldata _chainIds, address[] calldata _remoteRfqContracts)
external
onlyOwner
{
require(_chainIds.length == _remoteRfqContracts.length, "Rfq: length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
remoteRfqContracts[_chainIds[i]] = _remoteRfqContracts[i];
}
emit RfqContractsUpdated(_chainIds, _remoteRfqContracts);
}
function setFeePerc(uint64[] calldata _chainIds, uint32[] calldata _feePercs) external onlyGovernor {
require(_chainIds.length == _feePercs.length, "Rfq: length mismatch");
for (uint256 i = 0; i < _chainIds.length; i++) {
require(_feePercs[i] < 1e6, "Rfq: fee percentage too large");
if (_chainIds[i] == 0) {
feePercGlobal = _feePercs[i];
} else {
feePercOverride[_chainIds[i]] = _feePercs[i];
}
}
emit FeePercUpdated(_chainIds, _feePercs);
}
function setTreasuryAddr(address _treasuryAddr) external onlyOwner {
treasuryAddr = _treasuryAddr;
emit TreasuryAddrUpdated(_treasuryAddr);
}
function setNativeWrap(address _nativeWrap) external onlyOwner {
nativeWrap = _nativeWrap;
}
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyOwner {
nativeTokenTransferGas = _gasUsed;
}
}
================================================
FILE: contracts/message/apps/adapter/MessageReceiverAdapter.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "../../framework/MessageApp.sol";
import "../../safeguard/MessageAppPauser.sol";
import "../../safeguard/DelayedMessage.sol";
import "../../../libraries/Utils.sol";
contract MessageReceiverAdapter is MessageApp, MessageAppPauser, DelayedMessage {
event ExternalCall(address srcContract, uint64 srcChainId, address dstContract, bytes callData);
event AllowedSenderUpdated(address dstContract, uint64 srcChainId, address srcContract, bool allowed);
// dstContract => srcChainId => srcContract => allowed or not
mapping(address => mapping(uint64 => mapping(address => bool))) public allowedSender;
constructor(address _messageBus) MessageApp(_messageBus) {}
// Called by MessageBus on destination chain to receive cross-chain messages.
// The message is abi.encode of (dst_contract_address, dst_contract_calldata).
// If a delayed period is configured, the message would be put in a delayed message queue,
// otherwise, the external call to the dst contract will be executed immediately
function executeMessage(
address _srcContract,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus whenNotMsgPaused returns (ExecutionStatus) {
(address dstContract, bytes memory callData) = abi.decode(_message, (address, bytes));
require(allowedSender[dstContract][_srcChainId][_srcContract], "not allowed sender");
if (delayPeriod > 0) {
_addDelayedMessage(_srcContract, _srcChainId, _message);
} else {
_externalCall(_srcContract, _srcChainId, dstContract, callData);
}
return ExecutionStatus.Success;
}
// execute external call to the dst contract after the message delay period is passed.
function executeDelayedMessage(
address _srcContract,
uint64 _srcChainId,
bytes calldata _message,
uint32 _nonce
) external payable whenNotPaused {
_executeDelayedMessage(_srcContract, _srcChainId, _message, _nonce);
(address dstContract, bytes memory callData) = abi.decode(_message, (address, bytes));
_externalCall(_srcContract, _srcChainId, dstContract, callData);
}
function _externalCall(
address _srcContract,
uint64 _srcChainId,
address _dstContract,
bytes memory _callData
) internal {
(bool ok, bytes memory returnData) = _dstContract.call{value: msg.value}(_callData);
if (!ok) {
revert(Utils.getRevertMsg(returnData));
}
emit ExternalCall(_srcContract, _srcChainId, _dstContract, _callData);
}
function setAllowedSender(
address _dstContract,
uint64 _srcChainId,
address[] calldata _srcContracts,
bool[] calldata _alloweds
) external onlyOwner {
require(_srcContracts.length == _alloweds.length, "length mismatch");
for (uint256 i = 0; i < _srcContracts.length; i++) {
allowedSender[_dstContract][_srcChainId][_srcContracts[i]] = _alloweds[i];
emit AllowedSenderUpdated(_dstContract, _srcChainId, _srcContracts[i], _alloweds[i]);
}
}
}
================================================
FILE: contracts/message/apps/examples/BatchTransfer.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../../framework/MessageApp.sol";
/** @title Sample app to test message passing flow, not for production use */
contract BatchTransfer is MessageApp {
using SafeERC20 for IERC20;
struct TransferRequest {
uint64 nonce;
address[] accounts;
uint256[] amounts;
address sender;
}
enum TransferStatus {
Null,
Success,
Fail
}
struct TransferReceipt {
uint64 nonce;
TransferStatus status;
}
constructor(address _messageBus) MessageApp(_messageBus) {}
// ============== functions and states on source chain ==============
uint64 nonce;
struct BatchTransferStatus {
bytes32 h; // hash(receiver, dstChainId)
TransferStatus status;
}
mapping(uint64 => BatchTransferStatus) public status; // nonce -> BatchTransferStatus
modifier onlyEOA() {
require(msg.sender == tx.origin, "Not EOA");
_;
}
// called by sender on source chain to send tokens to a list of
// <_accounts, _amounts> on the destination chain
function batchTransfer(
address _dstContract, // BatchTransfer contract address at the dst chain
address _token,
uint256 _amount,
uint64 _dstChainId,
uint32 _maxSlippage,
MsgDataTypes.BridgeSendType _bridgeSendType,
address[] calldata _accounts,
uint256[] calldata _amounts
) external payable onlyEOA {
uint256 totalAmt;
for (uint256 i = 0; i < _amounts.length; i++) {
totalAmt += _amounts[i];
}
// commented out the slippage check below to trigger failure case for handleFailedMessageWithTransfer testing
// uint256 minRecv = _amount - (_amount * _maxSlippage) / 1e6;
// require(minRecv > totalAmt, "invalid maxSlippage");
nonce += 1;
status[nonce] = BatchTransferStatus({
h: keccak256(abi.encodePacked(_dstContract, _dstChainId)),
status: TransferStatus.Null
});
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
bytes memory message = abi.encode(
TransferRequest({nonce: nonce, accounts: _accounts, amounts: _amounts, sender: msg.sender})
);
// send token and message to the destination chain
sendMessageWithTransfer(
_dstContract,
_token,
_amount,
_dstChainId,
nonce,
_maxSlippage,
message,
_bridgeSendType,
msg.value
);
}
// called by MessageBus on source chain to handle message with token transfer failures (e.g., due to bad slippage).
// the associated token transfer is guaranteed to have already been refunded
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode((_message), (TransferRequest));
IERC20(_token).safeTransfer(transfer.sender, _amount);
return ExecutionStatus.Success;
}
// called by MessageBus on source chain to receive receipts
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferReceipt memory receipt = abi.decode((_message), (TransferReceipt));
require(status[receipt.nonce].h == keccak256(abi.encodePacked(_sender, _srcChainId)), "invalid message");
status[receipt.nonce].status = receipt.status;
return ExecutionStatus.Success;
}
// ============== functions on destination chain ==============
// called by MessageBus on destination chain to handle batchTransfer message by
// distributing tokens to receivers and sending receipt.
// the lump sum token transfer associated with the message is guaranteed to have already been received.
function executeMessageWithTransfer(
address _srcContract,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode((_message), (TransferRequest));
uint256 totalAmt;
for (uint256 i = 0; i < transfer.accounts.length; i++) {
IERC20(_token).safeTransfer(transfer.accounts[i], transfer.amounts[i]);
totalAmt += transfer.amounts[i];
}
uint256 remainder = _amount - totalAmt;
if (_amount > totalAmt) {
// transfer the remainder of the money to sender as fee for executing this transfer
IERC20(_token).safeTransfer(transfer.sender, remainder);
}
bytes memory message = abi.encode(TransferReceipt({nonce: transfer.nonce, status: TransferStatus.Success}));
// send receipt back to the source chain contract
sendMessage(_srcContract, _srcChainId, message, msg.value);
return ExecutionStatus.Success;
}
// called by MessageBus if handleMessageWithTransfer above got reverted
function executeMessageWithTransferFallback(
address _srcContract,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
TransferRequest memory transfer = abi.decode((_message), (TransferRequest));
IERC20(_token).safeTransfer(transfer.sender, _amount);
bytes memory message = abi.encode(TransferReceipt({nonce: transfer.nonce, status: TransferStatus.Fail}));
// send receipt back to the source chain contract
sendMessage(_srcContract, _srcChainId, message, msg.value);
return ExecutionStatus.Success;
}
}
================================================
FILE: contracts/message/apps/examples/MsgExampleBasic.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "../../framework/MessageApp.sol";
// A HelloWorld example for basic cross-chain message passing
contract MsgExampleBasic is MessageApp {
event MessageReceived(address srcContract, uint64 srcChainId, address sender, bytes message);
constructor(address _messageBus) MessageApp(_messageBus) {}
// called by user on source chain to send cross-chain messages
function sendMessage(
address _dstContract,
uint64 _dstChainId,
bytes calldata _message
) external payable {
bytes memory message = abi.encode(msg.sender, _message);
sendMessage(_dstContract, _dstChainId, message, msg.value);
}
// called by MessageBus on destination chain to receive cross-chain messages
function executeMessage(
address _srcContract,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address sender, bytes memory message) = abi.decode((_message), (address, bytes));
emit MessageReceived(_srcContract, _srcChainId, sender, message);
return ExecutionStatus.Success;
}
}
================================================
FILE: contracts/message/apps/examples/MsgExampleBasicTransfer.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../framework/MessageApp.sol";
// A HelloWorld example for basic cross-chain message passing with associate cross-chain token transfer
contract MsgExampleBasicTransfer is MessageApp {
using SafeERC20 for IERC20;
event MessageWithTransferReceived(address sender, address token, uint256 amount, uint64 srcChainId, bytes note);
event MessageWithTransferRefunded(address sender, address token, uint256 amount, bytes note);
// account, token -> balance
mapping(address => mapping(address => uint256)) public balances;
constructor(address _messageBus) MessageApp(_messageBus) {}
// called by user on source chain to send token with note to destination chain
function sendTokenWithNote(
address _dstContract,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
bytes calldata _note,
MsgDataTypes.BridgeSendType _bridgeSendType
) external payable {
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
bytes memory message = abi.encode(msg.sender, _note);
sendMessageWithTransfer(
_dstContract,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
message,
_bridgeSendType,
msg.value
);
}
// called by MessageBus on destination chain to receive message, record and emit info.
// the associated token transfer is guaranteed to have already been received
function executeMessageWithTransfer(
address, // srcContract
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address sender, bytes memory note) = abi.decode((_message), (address, bytes));
balances[sender][_token] += _amount;
emit MessageWithTransferReceived(sender, _token, _amount, _srcChainId, note);
return ExecutionStatus.Success;
}
// called by MessageBus on source chain to handle message with failed token transfer
// the associated token transfer is guaranteed to have already been refunded
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address sender, bytes memory note) = abi.decode((_message), (address, bytes));
IERC20(_token).safeTransfer(sender, _amount);
emit MessageWithTransferRefunded(sender, _token, _amount, note);
return ExecutionStatus.Success;
}
// called by user on destination chain to withdraw tokens
function withdraw(address _token, uint256 _amount) external {
balances[msg.sender][_token] -= _amount;
IERC20(_token).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/message/apps/examples/MsgExampleInOrder.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "../../framework/MessageApp.sol";
// a simple example to enforce in-order message delivery
contract MsgExampleInOrder is MessageApp {
event MessageReceived(address srcContract, uint64 srcChainId, address sender, uint64 seq, bytes message);
// map at source chain. (dstChainId, dstContract) -> seq
mapping(uint64 => mapping(address => uint64)) public sendSeq;
// map at destination chain (srcChainId, srcContract) -> seq
mapping(uint64 => mapping(address => uint64)) public recvSeq;
constructor(address _messageBus) MessageApp(_messageBus) {}
// called by user on source chain to send cross-chain message
function sendMessage(
address _dstContract,
uint64 _dstChainId,
bytes calldata _message
) external payable {
uint64 seq = sendSeq[_dstChainId][_dstContract];
bytes memory message = abi.encode(msg.sender, seq, _message);
sendMessage(_dstContract, _dstChainId, message, msg.value);
sendSeq[_dstChainId][_dstContract] += 1;
}
// called by MessageBus on destination chain to receive message
function executeMessage(
address _srcContract,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address sender, uint64 seq, bytes memory message) = abi.decode((_message), (address, uint64, bytes));
uint64 expectedSeq = recvSeq[_srcChainId][_srcContract];
if (seq != expectedSeq) {
// sequence number not expected, let executor retry.
// Note: cannot revert here, because once a message execute tx is reverted, it cannot be retried later.
return ExecutionStatus.Retry;
}
emit MessageReceived(_srcContract, _srcChainId, sender, seq, message);
recvSeq[_srcChainId][_srcContract] += 1;
return ExecutionStatus.Success;
}
}
================================================
FILE: contracts/message/apps/examples/MsgTest.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../framework/MessageApp.sol";
import "../../../safeguard/Ownable.sol";
/** @title Application to test message with transfer refund flow */
contract MsgTest is MessageApp, Ownable {
using SafeERC20 for IERC20;
uint64 nonce;
event MessageReceivedWithTransfer(
address token,
uint256 amount,
address sender,
uint64 srcChainId,
address receiver,
bytes message
);
event Refunded(address receiver, address token, uint256 amount, bytes message);
event MessageReceived(address sender, uint64 srcChainId, uint64 nonce, bytes message);
event Message2Received(bytes sender, uint64 srcChainId, uint64 nonce, bytes message);
constructor(address _messageBus) MessageApp(_messageBus) {}
function sendMessageWithTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint32 _maxSlippage,
bytes calldata _message,
MsgDataTypes.BridgeSendType _bridgeSendType
) external payable {
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
bytes memory message = abi.encode(msg.sender, _message);
sendMessageWithTransfer(
_receiver,
_token,
_amount,
_dstChainId,
nonce,
_maxSlippage,
message,
_bridgeSendType,
msg.value
);
nonce++;
}
function executeMessageWithTransfer(
address _sender,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address receiver, bytes memory message) = abi.decode((_message), (address, bytes));
IERC20(_token).safeTransfer(receiver, _amount);
emit MessageReceivedWithTransfer(_token, _amount, _sender, _srcChainId, receiver, message);
return ExecutionStatus.Success;
}
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(address receiver, bytes memory message) = abi.decode((_message), (address, bytes));
IERC20(_token).safeTransfer(receiver, _amount);
emit Refunded(receiver, _token, _amount, message);
return ExecutionStatus.Success;
}
function sendMessage(
address _receiver,
uint64 _dstChainId,
bytes calldata _message
) external payable {
bytes memory message = abi.encode(nonce, _message);
nonce++;
sendMessage(_receiver, _dstChainId, message, msg.value);
}
function sendMessage(
bytes calldata _receiver,
uint64 _dstChainId,
bytes calldata _message
) external payable {
bytes memory message = abi.encode(nonce, _message);
nonce++;
sendMessage(_receiver, _dstChainId, message, msg.value);
}
function sendMessages(
address _receiver,
uint64 _dstChainId,
bytes[] calldata _messages,
uint256[] calldata _fees
) external payable {
for (uint256 i = 0; i < _messages.length; i++) {
bytes memory message = abi.encode(nonce, _messages[i]);
nonce++;
sendMessage(_receiver, _dstChainId, message, _fees[i]);
}
}
function sendMessageWithNonce(
address _receiver,
uint64 _dstChainId,
bytes calldata _message,
uint64 _nonce
) external payable {
bytes memory message = abi.encode(_nonce, _message);
sendMessage(_receiver, _dstChainId, message, msg.value);
}
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(uint64 n, bytes memory message) = abi.decode((_message), (uint64, bytes));
require(n != 100000000000001, "invalid nonce"); // test revert with reason
if (n == 100000000000002) {
// test revert without reason
revert();
} else if (n == 100000000000003) {
return ExecutionStatus.Retry;
}
// test execution revert
require(n != 100000000000004, _abortReason("invalid nonce"));
emit MessageReceived(_sender, _srcChainId, n, message);
return ExecutionStatus.Success;
}
function executeMessage(
bytes calldata _sender,
uint64 _srcChainId,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
(uint64 n, bytes memory message) = abi.decode((_message), (uint64, bytes));
emit Message2Received(_sender, _srcChainId, n, message);
return ExecutionStatus.Success;
}
function drainToken(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/message/apps/examples/TransferSwap.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../../framework/MessageApp.sol";
import "../../../safeguard/Ownable.sol";
import "../../../interfaces/IWETH.sol";
import "../../../interfaces/IUniswapV2.sol";
/**
* @title Demo application contract that facilitates swapping on a chain, transferring to another chain,
* and swapping another time on the destination chain before sending the result tokens to a user
*/
contract TransferSwap is MessageApp, Ownable {
using SafeERC20 for IERC20;
modifier onlyEOA() {
require(msg.sender == tx.origin, "Not EOA");
_;
}
struct SwapInfo {
// if this array has only one element, it means no need to swap
address[] path;
// the following fields are only needed if path.length > 1
address dex; // the DEX to use for the swap
uint256 deadline; // deadline for the swap
uint256 minRecvAmt; // minimum receive amount for the swap
}
struct SwapRequest {
SwapInfo swap;
// the receiving party (the user) of the final output token
address receiver;
// this field is best to be per-user per-transaction unique so that
// a nonce that is specified by the calling party (the user),
uint64 nonce;
// indicates whether the output token coming out of the swap on destination
// chain should be unwrapped before sending to the user
bool nativeOut;
}
enum SwapStatus {
Null,
Succeeded,
Failed,
Fallback
}
// emitted when requested dstChainId == srcChainId, no bridging
event DirectSwap(
bytes32 id,
uint64 srcChainId,
uint256 amountIn,
address tokenIn,
uint256 amountOut,
address tokenOut
);
event SwapRequestSent(bytes32 id, uint64 dstChainId, uint256 srcAmount, address srcToken, address dstToken);
event SwapRequestDone(bytes32 id, uint256 dstAmount, SwapStatus status);
mapping(address => uint256) public minSwapAmounts;
mapping(address => bool) supportedDex;
// erc20 wrap of gas token of this chain, eg. WETH
address public nativeWrap;
uint256 public nativeTokenTransferGas = 50000;
constructor(
address _messageBus,
address _supportedDex,
address _nativeWrap
) MessageApp(_messageBus) {
supportedDex[_supportedDex] = true;
nativeWrap = _nativeWrap;
}
function transferWithSwapNative(
address _receiver,
uint256 _amountIn,
uint64 _dstChainId,
SwapInfo calldata _srcSwap,
SwapInfo calldata _dstSwap,
uint32 _maxBridgeSlippage,
uint64 _nonce,
bool _nativeOut
) external payable onlyEOA {
require(msg.value >= _amountIn, "Amount insufficient");
require(_srcSwap.path[0] == nativeWrap, "token mismatch");
IWETH(nativeWrap).deposit{value: _amountIn}();
_transferWithSwap(
_receiver,
_amountIn,
_dstChainId,
_srcSwap,
_dstSwap,
_maxBridgeSlippage,
_nonce,
_nativeOut,
msg.value - _amountIn
);
}
function transferWithSwap(
address _receiver,
uint256 _amountIn,
uint64 _dstChainId,
SwapInfo calldata _srcSwap,
SwapInfo calldata _dstSwap,
uint32 _maxBridgeSlippage,
uint64 _nonce
) external payable onlyEOA {
IERC20(_srcSwap.path[0]).safeTransferFrom(msg.sender, address(this), _amountIn);
_transferWithSwap(
_receiver,
_amountIn,
_dstChainId,
_srcSwap,
_dstSwap,
_maxBridgeSlippage,
_nonce,
false,
msg.value
);
}
/**
* @notice Sends a cross-chain transfer via the liquidity pool-based bridge and sends a message specifying a wanted swap action on the
destination chain via the message bus
* @param _receiver the app contract that implements the MessageReceiver abstract contract
* NOTE not to be confused with the receiver field in SwapInfo which is an EOA address of a user
* @param _amountIn the input amount that the user wants to swap and/or bridge
* @param _dstChainId destination chain ID
* @param _srcSwap a struct containing swap related requirements
* @param _dstSwap a struct containing swap related requirements
* @param _maxBridgeSlippage the max acceptable slippage at bridge, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded.
* @param _fee the fee to pay to MessageBus.
*/
function _transferWithSwap(
address _receiver,
uint256 _amountIn,
uint64 _dstChainId,
SwapInfo memory _srcSwap,
SwapInfo memory _dstSwap,
uint32 _maxBridgeSlippage,
uint64 _nonce,
bool _nativeOut,
uint256 _fee
) private {
require(_srcSwap.path.length > 0, "empty src swap path");
address srcTokenOut = _srcSwap.path[_srcSwap.path.length - 1];
require(_amountIn > minSwapAmounts[_srcSwap.path[0]], "amount must be greater than min swap amount");
uint64 chainId = uint64(block.chainid);
require(_srcSwap.path.length > 1 || _dstChainId != chainId, "noop is not allowed"); // revert early to save gas
uint256 srcAmtOut = _amountIn;
// swap source token for intermediate token on the source DEX
if (_srcSwap.path.length > 1) {
bool ok = true;
(ok, srcAmtOut) = _trySwap(_srcSwap, _amountIn);
if (!ok) revert("src swap failed");
}
if (_dstChainId == chainId) {
_directSend(_receiver, _amountIn, chainId, _srcSwap, _nonce, srcTokenOut, srcAmtOut);
} else {
_crossChainTransferWithSwap(
_receiver,
_amountIn,
chainId,
_dstChainId,
_srcSwap,
_dstSwap,
_maxBridgeSlippage,
_nonce,
_nativeOut,
_fee,
srcTokenOut,
srcAmtOut
);
}
}
function _directSend(
address _receiver,
uint256 _amountIn,
uint64 _chainId,
SwapInfo memory _srcSwap,
uint64 _nonce,
address srcTokenOut,
uint256 srcAmtOut
) private {
// no need to bridge, directly send the tokens to user
IERC20(srcTokenOut).safeTransfer(_receiver, srcAmtOut);
// use uint64 for chainid to be consistent with other components in the system
bytes32 id = keccak256(abi.encode(msg.sender, _chainId, _receiver, _nonce, _srcSwap));
emit DirectSwap(id, _chainId, _amountIn, _srcSwap.path[0], srcAmtOut, srcTokenOut);
}
function _crossChainTransferWithSwap(
address _receiver,
uint256 _amountIn,
uint64 _chainId,
uint64 _dstChainId,
SwapInfo memory _srcSwap,
SwapInfo memory _dstSwap,
uint32 _maxBridgeSlippage,
uint64 _nonce,
bool _nativeOut,
uint256 _fee,
address srcTokenOut,
uint256 srcAmtOut
) private {
require(_dstSwap.path.length > 0, "empty dst swap path");
bytes memory message = abi.encode(
SwapRequest({swap: _dstSwap, receiver: msg.sender, nonce: _nonce, nativeOut: _nativeOut})
);
bytes32 id = _computeSwapRequestId(msg.sender, _chainId, _dstChainId, message);
// bridge the intermediate token to destination chain along with the message
// NOTE In production, it's better use a per-user per-transaction nonce so that it's less likely transferId collision
// would happen at Bridge contract. Currently this nonce is a timestamp supplied by frontend
sendMessageWithTransfer(
_receiver,
srcTokenOut,
srcAmtOut,
_dstChainId,
_nonce,
_maxBridgeSlippage,
message,
MsgDataTypes.BridgeSendType.Liquidity,
_fee
);
emit SwapRequestSent(id, _dstChainId, _amountIn, _srcSwap.path[0], _dstSwap.path[_dstSwap.path.length - 1]);
}
/**
* @notice called by MessageBus when the tokens are checked to be arrived at this contract's address.
sends the amount received to the receiver. swaps beforehand if swap behavior is defined in message
* NOTE: if the swap fails, it sends the tokens received directly to the receiver as fallback behavior
* @param _token the address of the token sent through the bridge
* @param _amount the amount of tokens received at this contract through the cross-chain bridge
* @param _srcChainId source chain ID
* @param _message SwapRequest message that defines the swap behavior on this destination chain
*/
function executeMessageWithTransfer(
address, // _sender
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
SwapRequest memory m = abi.decode((_message), (SwapRequest));
require(_token == m.swap.path[0], "bridged token must be the same as the first token in destination swap path");
bytes32 id = _computeSwapRequestId(m.receiver, _srcChainId, uint64(block.chainid), _message);
uint256 dstAmount;
SwapStatus status = SwapStatus.Succeeded;
if (m.swap.path.length > 1) {
bool ok = true;
(ok, dstAmount) = _trySwap(m.swap, _amount);
if (ok) {
_sendToken(m.swap.path[m.swap.path.length - 1], dstAmount, m.receiver, m.nativeOut);
status = SwapStatus.Succeeded;
} else {
// handle swap failure, send the received token directly to receiver
_sendToken(_token, _amount, m.receiver, false);
dstAmount = _amount;
status = SwapStatus.Fallback;
}
} else {
// no need to swap, directly send the bridged token to user
_sendToken(m.swap.path[0], _amount, m.receiver, m.nativeOut);
dstAmount = _amount;
status = SwapStatus.Succeeded;
}
emit SwapRequestDone(id, dstAmount, status);
// always return success since swap failure is already handled in-place
return ExecutionStatus.Success;
}
/**
* @notice called by MessageBus when the executeMessageWithTransfer call fails. does nothing but emitting a "fail" event
* @param _srcChainId source chain ID
* @param _message SwapRequest message that defines the swap behavior on this destination chain
*/
function executeMessageWithTransferFallback(
address, // _sender
address, // _token
uint256, // _amount
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
SwapRequest memory m = abi.decode((_message), (SwapRequest));
bytes32 id = _computeSwapRequestId(m.receiver, _srcChainId, uint64(block.chainid), _message);
emit SwapRequestDone(id, 0, SwapStatus.Failed);
// always return fail to mark this transfer as failed since if this function is called then there nothing more
// we can do in this app as the swap failures are already handled in executeMessageWithTransfer
return ExecutionStatus.Fail;
}
function _trySwap(SwapInfo memory _swap, uint256 _amount) private returns (bool ok, uint256 amountOut) {
uint256 zero;
if (!supportedDex[_swap.dex]) {
return (false, zero);
}
IERC20(_swap.path[0]).safeIncreaseAllowance(_swap.dex, _amount);
try
IUniswapV2(_swap.dex).swapExactTokensForTokens(
_amount,
_swap.minRecvAmt,
_swap.path,
address(this),
_swap.deadline
)
returns (uint256[] memory amounts) {
return (true, amounts[amounts.length - 1]);
} catch {
return (false, zero);
}
}
function _sendToken(
address _token,
uint256 _amount,
address _receiver,
bool _nativeOut
) private {
if (_nativeOut) {
require(_token == nativeWrap, "token mismatch");
IWETH(nativeWrap).withdraw(_amount);
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native");
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
function _computeSwapRequestId(
address _sender,
uint64 _srcChainId,
uint64 _dstChainId,
bytes memory _message
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(_sender, _srcChainId, _dstChainId, _message));
}
function setMinSwapAmount(address _token, uint256 _minSwapAmount) external onlyOwner {
minSwapAmounts[_token] = _minSwapAmount;
}
function setSupportedDex(address _dex, bool _enabled) external onlyOwner {
supportedDex[_dex] = _enabled;
}
function setNativeWrap(address _nativeWrap) external onlyOwner {
nativeWrap = _nativeWrap;
}
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyOwner {
nativeTokenTransferGas = _gasUsed;
}
function setMessageBus(address _messageBus) public onlyOwner {
messageBus = _messageBus;
}
// This is needed to receive ETH when calling `IWETH.withdraw`
receive() external payable {}
}
================================================
FILE: contracts/message/apps/examples/TransferSwapSendBack.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "../../framework/MessageApp.sol";
interface ISwapToken {
// function sellBase(address to) external returns (uint256);
// uniswap v2
function swapExactTokensForTokens(
uint256,
uint256,
address[] calldata,
address,
uint256
) external returns (uint256[] memory);
}
contract CrossChainSwap is MessageApp {
using SafeERC20 for IERC20;
address public dex; // needed on swap chain
struct SwapInfo {
address wantToken; // token user want to receive on dest chain
address user;
bool sendBack; // if true, send wantToken back to start chain
uint32 cbrMaxSlippage; // _maxSlippage for cbridge send
}
constructor(address _messageBus, address dex_) MessageApp(_messageBus) {
dex = dex_;
}
// ========== on start chain ==========
uint64 nonce; // required by IBridge.send
// this func could be called by a router contract
function startCrossChainSwap(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
SwapInfo calldata swapInfo // wantToken on destChain and actual user address as receiver when send back
) external payable {
nonce += 1;
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
bytes memory message = abi.encode(swapInfo);
sendMessageWithTransfer(
_receiver,
_token,
_amount,
_dstChainId,
nonce,
swapInfo.cbrMaxSlippage,
message,
MsgDataTypes.BridgeSendType.Liquidity,
msg.value
);
}
// ========== on swap chain ==========
// do dex, send received asset to src chain via bridge
function executeMessageWithTransfer(
address, // _sender
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes memory _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
SwapInfo memory swapInfo = abi.decode((_message), (SwapInfo));
IERC20(_token).approve(dex, _amount);
address[] memory path = new address[](2);
path[0] = _token;
path[1] = swapInfo.wantToken;
if (swapInfo.sendBack) {
nonce += 1;
uint256[] memory swapReturn = ISwapToken(dex).swapExactTokensForTokens(
_amount,
0,
path,
address(this),
type(uint256).max
);
// send received token back to start chain. swapReturn[1] is amount of wantToken
sendTokenTransfer(
swapInfo.user,
swapInfo.wantToken,
swapReturn[1],
_srcChainId,
nonce,
swapInfo.cbrMaxSlippage,
MsgDataTypes.BridgeSendType.Liquidity
);
} else {
// swap to wantToken and send to user
ISwapToken(dex).swapExactTokensForTokens(_amount, 0, path, swapInfo.user, type(uint256).max);
}
// bytes memory notice; // send back to src chain to handleMessage
// sendMessage(_sender, _srcChainId, notice);
return ExecutionStatus.Success;
}
}
================================================
FILE: contracts/message/apps/nft-bridge/MCNNFT.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "../../../safeguard/Pauser.sol";
interface INFTBridge {
function sendMsg(
uint64 _dstChid,
address _sender,
address _receiver,
uint256 _id,
string calldata _uri
) external payable;
function sendMsg(
uint64 _dstChid,
address _sender,
bytes calldata _receiver,
uint256 _id,
string calldata _uri
) external payable;
function totalFee(
uint64 _dstChid,
address _nft,
uint256 _id
) external view returns (uint256);
}
// Multi-Chain Native NFT, same contract on all chains. User interacts with this directly.
contract MCNNFT is ERC721URIStorage, Pauser {
event NFTBridgeUpdated(address);
address public nftBridge;
constructor(
string memory name_,
string memory symbol_,
address _nftBridge
) ERC721(name_, symbol_) {
nftBridge = _nftBridge;
}
modifier onlyNftBridge() {
require(msg.sender == nftBridge, "caller is not bridge");
_;
}
function bridgeMint(
address to,
uint256 id,
string memory uri
) external onlyNftBridge {
_mint(to, id);
_setTokenURI(id, uri);
}
// calls nft bridge to get total fee for crossChain msg.Value
function totalFee(uint64 _dstChid, uint256 _id) external view returns (uint256) {
return INFTBridge(nftBridge).totalFee(_dstChid, address(this), _id);
}
// called by user, burn token on this chain and mint same id/uri on dest chain
function crossChain(
uint64 _dstChid,
uint256 _id,
address _receiver
) external payable whenNotPaused {
require(msg.sender == ownerOf(_id), "not token owner");
string memory _uri = tokenURI(_id);
_burn(_id);
INFTBridge(nftBridge).sendMsg{value: msg.value}(_dstChid, msg.sender, _receiver, _id, _uri);
}
// support chains using bytes for address
function crossChain(
uint64 _dstChid,
uint256 _id,
bytes calldata _receiver
) external payable whenNotPaused {
require(msg.sender == ownerOf(_id), "not token owner");
string memory _uri = tokenURI(_id);
_burn(_id);
INFTBridge(nftBridge).sendMsg{value: msg.value}(_dstChid, msg.sender, _receiver, _id, _uri);
}
// ===== only Owner
function mint(
address to,
uint256 id,
string memory uri
) external onlyOwner {
_mint(to, id);
_setTokenURI(id, uri);
}
function setNFTBridge(address _newBridge) public onlyOwner {
nftBridge = _newBridge;
emit NFTBridgeUpdated(_newBridge);
}
}
================================================
FILE: contracts/message/apps/nft-bridge/NFTBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../../framework/MessageReceiverApp.sol";
import "../../interfaces/IMessageBus.sol";
import "../../../safeguard/Pauser.sol";
// interface for NFT contract, ERC721 and metadata, only funcs needed by NFTBridge
interface INFT {
function tokenURI(uint256 tokenId) external view returns (string memory);
function ownerOf(uint256 tokenId) external view returns (address owner);
// we do not support NFT that charges transfer fees
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
// impl by NFToken contract, mint an NFT with id and uri to user or burn
function bridgeMint(
address to,
uint256 id,
string memory uri
) external;
function burn(uint256 id) external;
}
/** @title NFT Bridge */
contract NFTBridge is MessageReceiverApp, Pauser {
/// per dest chain id executor fee in this chain's gas token
mapping(uint64 => uint256) public destTxFee;
/// per dest chain id NFTBridge address
mapping(uint64 => address) public destBridge;
/// first key is NFT address on this chain, 2nd key is dest chain id, value is address on dest chain
mapping(address => mapping(uint64 => address)) public destNFTAddr;
/// only set to true if NFT addr on this chain is the orig, so we will use deposit/withdraw instead of burn/mint.
/// not applicable for mcn nft (always burn/mint)
mapping(address => bool) public origNFT;
/// only for non-evm chains and address can't fit 20bytes
mapping(uint64 => bytes) public destBridge2;
mapping(address => mapping(uint64 => bytes)) public destNFTAddr2;
struct NFTMsg {
address user; // receiver of minted or withdrawn NFT
address nft; // NFT contract on mint/withdraw chain
uint256 id; // token ID
string uri; // tokenURI from source NFT
}
// for non-evm dst chain, address type is bytes
struct NFTMsg2 {
bytes user; // receiver of minted or withdrawn NFT
bytes nft; // NFT contract on mint/withdraw chain
uint256 id; // token ID
string uri; // tokenURI from source NFT
}
// emit in deposit or burn
event Sent(address sender, address srcNft, uint256 id, uint64 dstChid, address receiver, address dstNft);
// bytes type for receiver and dstNft
event Sent2(address sender, address srcNft, uint256 id, uint64 dstChid, bytes receiver, bytes dstNft);
// emit for mint or withdraw message
event Received(address receiver, address nft, uint256 id, uint64 srcChid);
// emit when params change
event SetDestNFT(address srcNft, uint64 dstChid, address dstNft);
event SetTxFee(uint64 chid, uint256 fee);
event SetDestBridge(uint64 dstChid, address dstNftBridge);
event FeeClaimed(uint256 amount);
event SetOrigNFT(address nft, bool isOrig);
// emit if executeMessage calls nft transfer or bridgeMint returns error
event ExtCallErr(bytes returnData);
event SetDestNFT2(address srcNft, uint64 dstChid, bytes dstNft);
event SetDestBridge2(uint64 dstChid, bytes dstNftBridge);
constructor(address _msgBus) {
messageBus = _msgBus;
}
// only to be called by Proxy via delegatecall and will modify Proxy state
// initOwner will fail if owner is already set, so only delegateCall will work
function init(address _msgBus) external {
initOwner();
messageBus = _msgBus;
}
/**
* @notice totalFee returns gas token value to be set in user tx, includes both msg fee and executor fee for dest chain
* @dev we assume if dst chain address are bytes, user and nft are same length, otherwise we need to add receiver to args
* @param _dstChid dest chain ID
* @param _nft address of source NFT contract
* @param _id token ID to bridge (need to get accurate tokenURI length)
* @return total fee needed for user tx
*/
function totalFee(
uint64 _dstChid,
address _nft,
uint256 _id
) external view returns (uint256) {
string memory _uri = INFT(_nft).tokenURI(_id);
bytes memory message;
// try non-evm first
bytes memory dstNft = destNFTAddr2[_nft][_dstChid];
if (dstNft.length > 0) {
message = abi.encode(NFTMsg2(dstNft, dstNft, _id, _uri));
} else {
// evm chains or not configured, assume to be evm, 20 bytes address
message = abi.encode(NFTMsg(_nft, _nft, _id, _uri));
}
return IMessageBus(messageBus).calcFee(message) + destTxFee[_dstChid];
}
// ===== called by user
/**
* @notice locks or burn user's NFT in this contract and send message to mint (or withdraw) on dest chain
* @param _nft address of source NFT contract
* @param _id nft token ID to bridge
* @param _dstChid dest chain ID
* @param _receiver receiver address on dest chain
*/
function sendTo(
address _nft,
uint256 _id,
uint64 _dstChid,
address _receiver
) external payable whenNotPaused {
require(msg.sender == INFT(_nft).ownerOf(_id), "not token owner");
// must save _uri before burn
string memory _uri = INFT(_nft).tokenURI(_id);
lockOrBurn(_nft, _id);
(address _dstBridge, address _dstNft) = checkAddr(_nft, _dstChid);
msgBus(_dstBridge, _dstChid, abi.encode(NFTMsg(_receiver, _dstNft, _id, _uri)));
emit Sent(msg.sender, _nft, _id, _dstChid, _receiver, _dstNft);
}
/**
* @notice locks or burn user's NFT in this contract and send message to mint (or withdraw) on dest chain
* @param _nft address of source NFT contract
* @param _id nft token ID to bridge
* @param _dstChid dest chain ID
* @param _receiver receiver address on dest chain, arbitrary bytes
*/
function sendTo(
address _nft,
uint256 _id,
uint64 _dstChid,
bytes calldata _receiver
) external payable whenNotPaused {
require(msg.sender == INFT(_nft).ownerOf(_id), "not token owner");
// must save _uri before burn
string memory _uri = INFT(_nft).tokenURI(_id);
lockOrBurn(_nft, _id);
(bytes memory _dstBridge, bytes memory _dstNft) = checkAddr2(_nft, _dstChid);
msgBus(_dstBridge, _dstChid, abi.encode(NFTMsg2(_receiver, _dstNft, _id, _uri)));
emit Sent2(msg.sender, _nft, _id, _dstChid, _receiver, _dstNft);
}
// ===== called by MCN NFT after NFT is burnt
function sendMsg(
uint64 _dstChid,
address _sender,
address _receiver,
uint256 _id,
string calldata _uri
) external payable whenNotPaused {
address _nft = msg.sender;
(address _dstBridge, address _dstNft) = checkAddr(_nft, _dstChid);
msgBus(_dstBridge, _dstChid, abi.encode(NFTMsg(_receiver, _dstNft, _id, _uri)));
emit Sent(_sender, _nft, _id, _dstChid, _receiver, _dstNft);
}
// for non-evm chains and address can't fit 20bytes or non-hex
function sendMsg(
uint64 _dstChid,
address _sender,
bytes calldata _receiver,
uint256 _id,
string calldata _uri
) external payable whenNotPaused {
address _nft = msg.sender;
(bytes memory _dstBridge, bytes memory _dstNft) = checkAddr2(_nft, _dstChid);
msgBus(_dstBridge, _dstChid, abi.encode(NFTMsg2(_receiver, _dstNft, _id, _uri)));
emit Sent2(_sender, _nft, _id, _dstChid, _receiver, _dstNft);
}
// ===== called by msgbus
function executeMessage(
address sender,
uint64 srcChid,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
// Must check sender to ensure msg is from another nft bridge
// but we allow retry later in case it's a temporary config error
// risk is invalid sender will be retried but this can be easily filtered
// in executor or require manual trigger for retry
if (paused() || sender != destBridge[srcChid]) {
return ExecutionStatus.Retry;
}
return xferOrMint(_message, srcChid);
}
function executeMessage(
bytes calldata sender,
uint64 srcChid,
bytes calldata _message,
address // executor
) external payable override onlyMessageBus returns (ExecutionStatus) {
if (paused() || keccak256(sender) != keccak256(destBridge2[srcChid])) {
return ExecutionStatus.Retry;
}
return xferOrMint(_message, srcChid);
}
// ===== internal utils
// lockOrBurn on sender side
function lockOrBurn(address _nft, uint256 _id) internal {
if (origNFT[_nft] == true) {
// deposit
INFT(_nft).transferFrom(msg.sender, address(this), _id);
require(INFT(_nft).ownerOf(_id) == address(this), "transfer NFT failed");
} else {
// burn
INFT(_nft).burn(_id);
}
}
// xferOrMint on receiver side, transfer or mint NFT to receiver
function xferOrMint(bytes calldata _message, uint64 srcChid) internal returns (ExecutionStatus) {
// withdraw original locked nft back to user, or mint new nft depending on if this is the orig chain of nft
NFTMsg memory nftMsg = abi.decode((_message), (NFTMsg));
// if we are on nft orig chain, use transfer, otherwise, use mint
// we must never return fail because burnt nft will be lost forever
if (origNFT[nftMsg.nft] == true) {
try INFT(nftMsg.nft).transferFrom(address(this), nftMsg.user, nftMsg.id) {
// do nothing here to move on to emit Received event and return success
} catch (bytes memory returnData) {
emit ExtCallErr(returnData);
return ExecutionStatus.Retry;
}
} else {
try INFT(nftMsg.nft).bridgeMint(nftMsg.user, nftMsg.id, nftMsg.uri) {
// do nothing here to move on to emit Received event and return success
} catch (bytes memory returnData) {
emit ExtCallErr(returnData);
return ExecutionStatus.Retry;
}
}
emit Received(nftMsg.user, nftMsg.nft, nftMsg.id, srcChid);
return ExecutionStatus.Success;
}
// check _nft and destChid are valid, return dstBridge and dstNft
function checkAddr(address _nft, uint64 _dstChid) internal view returns (address dstBridge, address dstNft) {
dstBridge = destBridge[_dstChid];
require(dstBridge != address(0), "dest NFT Bridge not found");
dstNft = destNFTAddr[_nft][_dstChid];
require(dstNft != address(0), "dest NFT not found");
}
function checkAddr2(address _nft, uint64 _dstChid)
internal
view
returns (bytes memory dstBridge, bytes memory dstNft)
{
dstBridge = destBridge2[_dstChid];
require(dstBridge.length != 0, "dest NFT Bridge not found");
dstNft = destNFTAddr2[_nft][_dstChid];
require(dstNft.length != 0, "dest NFT not found");
}
// check fee and call msgbus sendMessage
function msgBus(
address _dstBridge,
uint64 _dstChid,
bytes memory message
) internal {
uint256 fee = IMessageBus(messageBus).calcFee(message);
require(msg.value >= fee + destTxFee[_dstChid], "insufficient fee");
IMessageBus(messageBus).sendMessage{value: fee}(_dstBridge, _dstChid, message);
}
function msgBus(
bytes memory _dstBridge,
uint64 _dstChid,
bytes memory message
) internal {
uint256 fee = IMessageBus(messageBus).calcFee(message);
require(msg.value >= fee + destTxFee[_dstChid], "insufficient fee");
IMessageBus(messageBus).sendMessage{value: fee}(_dstBridge, _dstChid, message);
}
// only owner
// set per NFT, per chain id, address
function setDestNFT(
address srcNft,
uint64 dstChid,
address dstNft
) external onlyOwner {
destNFTAddr[srcNft][dstChid] = dstNft;
emit SetDestNFT(srcNft, dstChid, dstNft);
}
// add to destNFTAddr2
function setDestNFT(
address srcNft,
uint64 dstChid,
bytes calldata dstNft
) external onlyOwner {
destNFTAddr2[srcNft][dstChid] = dstNft;
emit SetDestNFT2(srcNft, dstChid, dstNft);
}
// set all dest chains
function setDestNFTs(
address srcNft,
uint64[] calldata dstChid,
address[] calldata dstNft
) external onlyOwner {
require(dstChid.length == dstNft.length, "length mismatch");
for (uint256 i = 0; i < dstChid.length; i++) {
destNFTAddr[srcNft][dstChid[i]] = dstNft[i];
}
}
// set destTxFee
function setTxFee(uint64 chid, uint256 fee) external onlyOwner {
destTxFee[chid] = fee;
emit SetTxFee(chid, fee);
}
// set per chain id, nft bridge address
function setDestBridge(uint64 dstChid, address dstNftBridge) external onlyOwner {
destBridge[dstChid] = dstNftBridge;
emit SetDestBridge(dstChid, dstNftBridge);
}
function setDestBridge(uint64 dstChid, bytes calldata dstNftBridge) external onlyOwner {
destBridge2[dstChid] = dstNftBridge;
emit SetDestBridge2(dstChid, dstNftBridge);
}
// batch set nft bridge addresses for multiple chainids
function setDestBridges(uint64[] calldata dstChid, address[] calldata dstNftBridge) external onlyOwner {
for (uint256 i = 0; i < dstChid.length; i++) {
destBridge[dstChid[i]] = dstNftBridge[i];
}
}
// only called on NFT's orig chain, not applicable for mcn nft
function setOrigNFT(address _nft) external onlyOwner {
origNFT[_nft] = true;
emit SetOrigNFT(_nft, true);
}
// remove origNFT entry
function delOrigNFT(address _nft) external onlyOwner {
delete origNFT[_nft];
emit SetOrigNFT(_nft, false);
}
// send all gas token this contract has to owner
function claimFee() external onlyOwner {
uint256 amount = address(this).balance;
payable(msg.sender).transfer(amount);
emit FeeClaimed(amount);
}
}
================================================
FILE: contracts/message/apps/nft-bridge/OrigNFT.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "../../../safeguard/Ownable.sol";
contract OrigNFT is ERC721URIStorage, Ownable {
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}
function mint(
address to,
uint256 id,
string memory uri
) external onlyOwner {
_mint(to, id);
_setTokenURI(id, uri);
}
}
================================================
FILE: contracts/message/apps/nft-bridge/PegNFT.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract PegNFT is ERC721URIStorage {
address public immutable nftBridge;
constructor(
string memory name_,
string memory symbol_,
address _nftBridge
) ERC721(name_, symbol_) {
nftBridge = _nftBridge;
}
modifier onlyNftBridge() {
require(msg.sender == nftBridge, "caller is not bridge");
_;
}
function bridgeMint(
address to,
uint256 id,
string memory uri
) external onlyNftBridge {
_mint(to, id);
_setTokenURI(id, uri);
}
function burn(uint256 id) external onlyNftBridge {
_burn(id);
}
}
================================================
FILE: contracts/message/framework/MessageApp.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "./MessageSenderApp.sol";
import "./MessageReceiverApp.sol";
abstract contract MessageApp is MessageSenderApp, MessageReceiverApp {
constructor(address _messageBus) {
messageBus = _messageBus;
}
}
================================================
FILE: contracts/message/framework/MessageBusAddress.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
abstract contract MessageBusAddress {
address public messageBus;
}
================================================
FILE: contracts/message/framework/MessageReceiverApp.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "../interfaces/IMessageReceiverApp.sol";
import "../libraries/MsgDataTypes.sol";
import "./MessageBusAddress.sol";
abstract contract MessageReceiverApp is IMessageReceiverApp, MessageBusAddress {
modifier onlyMessageBus() {
require(msg.sender == messageBus, "caller is not message bus");
_;
}
// Add abort prefix in the reason string for require or revert.
// This will abort (revert) the message execution without markig it as failed state,
// making it possible to retry later.
function _abortReason(string memory reason) internal pure returns (string memory) {
return MsgDataTypes.abortReason(reason);
}
/**
* @notice Called by MessageBus to execute a message
* @param _sender The address of the source app contract
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable virtual override onlyMessageBus returns (ExecutionStatus) {}
// execute message from non-evm chain with bytes for sender address,
// otherwise same as above.
function executeMessage(
bytes calldata _sender,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable virtual override onlyMessageBus returns (ExecutionStatus) {}
/**
* @notice Called by MessageBus to execute a message with an associated token transfer.
* The contract is guaranteed to have received the right amount of tokens before this function is called.
* @param _sender The address of the source app contract
* @param _token The address of the token that comes out of the bridge
* @param _amount The amount of tokens received at this contract through the cross-chain bridge.
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransfer(
address _sender,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable virtual override onlyMessageBus returns (ExecutionStatus) {}
/**
* @notice Only called by MessageBus if
* 1. executeMessageWithTransfer reverts, or
* 2. executeMessageWithTransfer returns ExecutionStatus.Fail
* The contract is guaranteed to have received the right amount of tokens before this function is called.
* @param _sender The address of the source app contract
* @param _token The address of the token that comes out of the bridge
* @param _amount The amount of tokens received at this contract through the cross-chain bridge.
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransferFallback(
address _sender,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable virtual override onlyMessageBus returns (ExecutionStatus) {}
/**
* @notice Called by MessageBus to process refund of the original transfer from this contract.
* The contract is guaranteed to have received the refund before this function is called.
* @param _token The token address of the original transfer
* @param _amount The amount of the original transfer
* @param _message The same message associated with the original transfer
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address _executor
) external payable virtual override onlyMessageBus returns (ExecutionStatus) {}
}
================================================
FILE: contracts/message/framework/MessageSenderApp.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../libraries/MsgDataTypes.sol";
import "../libraries/MessageSenderLib.sol";
import "../messagebus/MessageBus.sol";
import "./MessageBusAddress.sol";
abstract contract MessageSenderApp is MessageBusAddress {
using SafeERC20 for IERC20;
// ============== Utility functions called by apps ==============
/**
* @notice Sends a message to a contract on another chain.
* Sender needs to make sure the uniqueness of the message Id, which is computed as
* hash(type.MessageOnly, sender, receiver, srcChainId, srcTxHash, dstChainId, message).
* If messages with the same Id are sent, only one of them will succeed at dst chain.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* @param _fee The fee amount to pay to MessageBus.
*/
function sendMessage(
address _receiver,
uint64 _dstChainId,
bytes memory _message,
uint256 _fee
) internal {
MessageSenderLib.sendMessage(_receiver, _dstChainId, _message, messageBus, _fee);
}
// Send message to non-evm chain with bytes for receiver address,
// otherwise same as above.
function sendMessage(
bytes calldata _receiver,
uint64 _dstChainId,
bytes memory _message,
uint256 _fee
) internal {
MessageSenderLib.sendMessage(_receiver, _dstChainId, _message, messageBus, _fee);
}
/**
* @notice Sends a message associated with a transfer to a contract on another chain.
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {MsgDataTypes.BridgeSendType.Liquidity}.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* If message is empty, only the token transfer will be sent
* @param _bridgeSendType One of the {BridgeSendType} enum.
* @param _fee The fee amount to pay to MessageBus.
* @return The transfer ID.
*/
function sendMessageWithTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
bytes memory _message,
MsgDataTypes.BridgeSendType _bridgeSendType,
uint256 _fee
) internal returns (bytes32) {
return
MessageSenderLib.sendMessageWithTransfer(
_receiver,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_message,
_bridgeSendType,
messageBus,
_fee
);
}
/**
* @notice Sends a token transfer via a bridge.
* @dev sendMessageWithTransfer with empty message
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {MsgDataTypes.BridgeSendType.Liquidity}.
* @param _bridgeSendType One of the {BridgeSendType} enum.
*/
function sendTokenTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
MsgDataTypes.BridgeSendType _bridgeSendType
) internal returns (bytes32) {
return
MessageSenderLib.sendMessageWithTransfer(
_receiver,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
"", // empty message, which will not trigger sendMessage
_bridgeSendType,
messageBus,
0
);
}
}
================================================
FILE: contracts/message/interfaces/IMessageBus.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "../libraries/MsgDataTypes.sol";
interface IMessageBus {
/**
* @notice Send a message to a contract on another chain.
* Sender needs to make sure the uniqueness of the message Id, which is computed as
* hash(type.MessageOnly, sender, receiver, srcChainId, srcTxHash, dstChainId, message).
* If messages with the same Id are sent, only one of them will succeed at dst chain..
* A fee is charged in the native gas token.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
*/
function sendMessage(
address _receiver,
uint256 _dstChainId,
bytes calldata _message
) external payable;
// same as above, except that receiver is an non-evm chain address,
function sendMessage(
bytes calldata _receiver,
uint256 _dstChainId,
bytes calldata _message
) external payable;
/**
* @notice Send a message associated with a token transfer to a contract on another chain.
* If messages with the same srcTransferId are sent, only one of them will succeed at dst chain..
* A fee is charged in the native token.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _srcBridge The bridge contract to send the transfer with.
* @param _srcTransferId The transfer ID.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
*/
function sendMessageWithTransfer(
address _receiver,
uint256 _dstChainId,
address _srcBridge,
bytes32 _srcTransferId,
bytes calldata _message
) external payable;
/**
* @notice Execute a message not associated with a transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessage(
bytes calldata _message,
MsgDataTypes.RouteInfo calldata _route,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external payable;
/**
* @notice Execute a message with a successful transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _transfer The transfer info.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessageWithTransfer(
bytes calldata _message,
MsgDataTypes.TransferInfo calldata _transfer,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external payable;
/**
* @notice Execute a message with a refunded transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _transfer The transfer info.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessageWithTransferRefund(
bytes calldata _message, // the same message associated with the original transfer
MsgDataTypes.TransferInfo calldata _transfer,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external payable;
/**
* @notice Withdraws message fee in the form of native gas token.
* @param _account The address receiving the fee.
* @param _cumulativeFee The cumulative fee credited to the account. Tracked by SGN.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A withdrawal must be
* signed-off by +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdrawFee(
address _account,
uint256 _cumulativeFee,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external;
/**
* @notice Calculates the required fee for the message.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
@ @return The required fee.
*/
function calcFee(bytes calldata _message) external view returns (uint256);
function liquidityBridge() external view returns (address);
function pegBridge() external view returns (address);
function pegBridgeV2() external view returns (address);
function pegVault() external view returns (address);
function pegVaultV2() external view returns (address);
}
================================================
FILE: contracts/message/interfaces/IMessageReceiverApp.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
interface IMessageReceiverApp {
enum ExecutionStatus {
Fail, // execution failed, finalized
Success, // execution succeeded, finalized
Retry // execution rejected, can retry later
}
/**
* @notice Called by MessageBus to execute a message
* @param _sender The address of the source app contract
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessage(
address _sender,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable returns (ExecutionStatus);
// same as above, except that sender is an non-evm chain address,
// otherwise same as above.
function executeMessage(
bytes calldata _sender,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable returns (ExecutionStatus);
/**
* @notice Called by MessageBus to execute a message with an associated token transfer.
* The contract is guaranteed to have received the right amount of tokens before this function is called.
* @param _sender The address of the source app contract
* @param _token The address of the token that comes out of the bridge
* @param _amount The amount of tokens received at this contract through the cross-chain bridge.
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransfer(
address _sender,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable returns (ExecutionStatus);
/**
* @notice Only called by MessageBus if
* 1. executeMessageWithTransfer reverts, or
* 2. executeMessageWithTransfer returns ExecutionStatus.Fail
* The contract is guaranteed to have received the right amount of tokens before this function is called.
* @param _sender The address of the source app contract
* @param _token The address of the token that comes out of the bridge
* @param _amount The amount of tokens received at this contract through the cross-chain bridge.
* @param _srcChainId The source chain ID where the transfer is originated from
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransferFallback(
address _sender,
address _token,
uint256 _amount,
uint64 _srcChainId,
bytes calldata _message,
address _executor
) external payable returns (ExecutionStatus);
/**
* @notice Called by MessageBus to process refund of the original transfer from this contract.
* The contract is guaranteed to have received the refund before this function is called.
* @param _token The token address of the original transfer
* @param _amount The amount of the original transfer
* @param _message The same message associated with the original transfer
* @param _executor Address who called the MessageBus execution function
*/
function executeMessageWithTransferRefund(
address _token,
uint256 _amount,
bytes calldata _message,
address _executor
) external payable returns (ExecutionStatus);
}
================================================
FILE: contracts/message/libraries/MessageSenderLib.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../../interfaces/IBridge.sol";
import "../../interfaces/IOriginalTokenVault.sol";
import "../../interfaces/IOriginalTokenVaultV2.sol";
import "../../interfaces/IPeggedTokenBridge.sol";
import "../../interfaces/IPeggedTokenBridgeV2.sol";
import "../interfaces/IMessageBus.sol";
import "./MsgDataTypes.sol";
library MessageSenderLib {
using SafeERC20 for IERC20;
// ============== Internal library functions called by apps ==============
/**
* @notice Sends a message to an app on another chain via MessageBus without an associated transfer.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* @param _messageBus The address of the MessageBus on this chain.
* @param _fee The fee amount to pay to MessageBus.
*/
function sendMessage(
address _receiver,
uint64 _dstChainId,
bytes memory _message,
address _messageBus,
uint256 _fee
) internal {
IMessageBus(_messageBus).sendMessage{value: _fee}(_receiver, _dstChainId, _message);
}
// Send message to non-evm chain with bytes for receiver address,
// otherwise same as above.
function sendMessage(
bytes calldata _receiver,
uint64 _dstChainId,
bytes memory _message,
address _messageBus,
uint256 _fee
) internal {
IMessageBus(_messageBus).sendMessage{value: _fee}(_receiver, _dstChainId, _message);
}
/**
* @notice Sends a message to an app on another chain via MessageBus with an associated transfer.
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded. Only applicable to the {MsgDataTypes.BridgeSendType.Liquidity}.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
* @param _bridgeSendType One of the {MsgDataTypes.BridgeSendType} enum.
* @param _messageBus The address of the MessageBus on this chain.
* @param _fee The fee amount to pay to MessageBus.
* @return The transfer ID.
*/
function sendMessageWithTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
bytes memory _message,
MsgDataTypes.BridgeSendType _bridgeSendType,
address _messageBus,
uint256 _fee
) internal returns (bytes32) {
(bytes32 transferId, address bridge) = sendTokenTransfer(
_receiver,
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_bridgeSendType,
_messageBus
);
if (_message.length > 0) {
IMessageBus(_messageBus).sendMessageWithTransfer{value: _fee}(
_receiver,
_dstChainId,
bridge,
transferId,
_message
);
}
return transferId;
}
/**
* @notice Sends a token transfer via a bridge.
* @param _receiver The address of the destination app contract.
* @param _token The address of the token to be sent.
* @param _amount The amount of tokens to be sent.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the
* transfer can be refunded.
* @param _bridgeSendType One of the {MsgDataTypes.BridgeSendType} enum.
*/
function sendTokenTransfer(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage,
MsgDataTypes.BridgeSendType _bridgeSendType,
address _messageBus
) internal returns (bytes32 transferId, address bridge) {
if (_bridgeSendType == MsgDataTypes.BridgeSendType.Liquidity) {
bridge = IMessageBus(_messageBus).liquidityBridge();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IBridge(bridge).send(_receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage);
transferId = computeLiqBridgeTransferId(_receiver, _token, _amount, _dstChainId, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegDeposit) {
bridge = IMessageBus(_messageBus).pegVault();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IOriginalTokenVault(bridge).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
transferId = computePegV1DepositId(_receiver, _token, _amount, _dstChainId, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegBurn) {
bridge = IMessageBus(_messageBus).pegBridge();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
IPeggedTokenBridge(bridge).burn(_token, _amount, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
transferId = computePegV1BurnId(_receiver, _token, _amount, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2Deposit) {
bridge = IMessageBus(_messageBus).pegVaultV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IOriginalTokenVaultV2(bridge).deposit(_token, _amount, _dstChainId, _receiver, _nonce);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2Burn) {
bridge = IMessageBus(_messageBus).pegBridgeV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IPeggedTokenBridgeV2(bridge).burn(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
} else if (_bridgeSendType == MsgDataTypes.BridgeSendType.PegV2BurnFrom) {
bridge = IMessageBus(_messageBus).pegBridgeV2();
IERC20(_token).safeIncreaseAllowance(bridge, _amount);
transferId = IPeggedTokenBridgeV2(bridge).burnFrom(_token, _amount, _dstChainId, _receiver, _nonce);
// handle cases where certain tokens do not spend allowance for role-based burn
IERC20(_token).safeApprove(bridge, 0);
} else {
revert("bridge type not supported");
}
}
function computeLiqBridgeTransferId(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce
) internal view returns (bytes32) {
return
keccak256(
abi.encodePacked(address(this), _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid))
);
}
function computePegV1DepositId(
address _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce
) internal view returns (bytes32) {
return
keccak256(
abi.encodePacked(address(this), _token, _amount, _dstChainId, _receiver, _nonce, uint64(block.chainid))
);
}
function computePegV1BurnId(
address _receiver,
address _token,
uint256 _amount,
uint64 _nonce
) internal view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), _token, _amount, _receiver, _nonce, uint64(block.chainid)));
}
}
================================================
FILE: contracts/message/libraries/MsgDataTypes.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
library MsgDataTypes {
string constant ABORT_PREFIX = "MSG::ABORT:";
// Add abort prefix in the reason string for require or revert.
// This will abort (revert) the message execution without markig it as failed state,
// making it possible to retry later.
function abortReason(string memory reason) internal pure returns (string memory) {
return string.concat(MsgDataTypes.ABORT_PREFIX, reason);
}
// bridge operation type at the sender side (src chain)
enum BridgeSendType {
Null,
Liquidity,
PegDeposit,
PegBurn,
PegV2Deposit,
PegV2Burn,
PegV2BurnFrom
}
// bridge operation type at the receiver side (dst chain)
enum TransferType {
Null,
LqRelay, // relay through liquidity bridge
LqWithdraw, // withdraw from liquidity bridge
PegMint, // mint through pegged token bridge
PegWithdraw, // withdraw from original token vault
PegV2Mint, // mint through pegged token bridge v2
PegV2Withdraw // withdraw from original token vault v2
}
enum MsgType {
MessageWithTransfer,
MessageOnly
}
enum TxStatus {
Null,
Success,
Fail,
Fallback,
Pending // transient state within a transaction
}
struct TransferInfo {
TransferType t;
address sender;
address receiver;
address token;
uint256 amount;
uint64 wdseq; // only needed for LqWithdraw (refund)
uint64 srcChainId;
bytes32 refId;
bytes32 srcTxHash; // src chain msg tx hash
}
struct RouteInfo {
address sender;
address receiver;
uint64 srcChainId;
bytes32 srcTxHash; // src chain msg tx hash
}
// used for msg from non-evm chains with longer-bytes address
struct RouteInfo2 {
bytes sender;
address receiver;
uint64 srcChainId;
bytes32 srcTxHash;
}
// combination of RouteInfo and RouteInfo2 for easier processing
struct Route {
address sender; // from RouteInfo
bytes senderBytes; // from RouteInfo2
address receiver;
uint64 srcChainId;
bytes32 srcTxHash;
}
struct MsgWithTransferExecutionParams {
bytes message;
TransferInfo transfer;
bytes[] sigs;
address[] signers;
uint256[] powers;
}
struct BridgeTransferParams {
bytes request;
bytes[] sigs;
address[] signers;
uint256[] powers;
}
}
================================================
FILE: contracts/message/messagebus/MessageBus.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./MessageBusSender.sol";
import "./MessageBusReceiver.sol";
contract MessageBus is MessageBusSender, MessageBusReceiver {
constructor(
ISigsVerifier _sigsVerifier,
address _liquidityBridge,
address _pegBridge,
address _pegVault,
address _pegBridgeV2,
address _pegVaultV2
)
MessageBusSender(_sigsVerifier)
MessageBusReceiver(_liquidityBridge, _pegBridge, _pegVault, _pegBridgeV2, _pegVaultV2)
{}
// this is only to be called by Proxy via delegateCall as initOwner will require _owner is 0.
// so calling init on this contract directly will guarantee to fail
function init(
address _liquidityBridge,
address _pegBridge,
address _pegVault,
address _pegBridgeV2,
address _pegVaultV2
) external {
// MUST manually call ownable init and must only call once
initOwner();
// we don't need sender init as _sigsVerifier is immutable so already in the deployed code
initReceiver(_liquidityBridge, _pegBridge, _pegVault, _pegBridgeV2, _pegVaultV2);
}
}
================================================
FILE: contracts/message/messagebus/MessageBusReceiver.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "../libraries/MsgDataTypes.sol";
import "../interfaces/IMessageReceiverApp.sol";
import "../../interfaces/IBridge.sol";
import "../../interfaces/IOriginalTokenVault.sol";
import "../../interfaces/IOriginalTokenVaultV2.sol";
import "../../interfaces/IPeggedTokenBridge.sol";
import "../../interfaces/IPeggedTokenBridgeV2.sol";
import "../../interfaces/IDelayedTransfer.sol";
import "../../safeguard/Ownable.sol";
import "../../libraries/Utils.sol";
contract MessageBusReceiver is Ownable {
mapping(bytes32 => MsgDataTypes.TxStatus) public executedMessages;
address public liquidityBridge; // liquidity bridge address
address public pegBridge; // peg bridge address
address public pegVault; // peg original vault address
address public pegBridgeV2; // peg bridge address
address public pegVaultV2; // peg original vault address
// minimum amount of gas needed by this contract before it tries to
// deliver a message to the target contract.
uint256 public preExecuteMessageGasUsage;
event Executed(
MsgDataTypes.MsgType msgType,
bytes32 msgId,
MsgDataTypes.TxStatus status,
address indexed receiver,
uint64 srcChainId,
bytes32 srcTxHash
);
event NeedRetry(MsgDataTypes.MsgType msgType, bytes32 msgId, uint64 srcChainId, bytes32 srcTxHash);
event CallReverted(string reason); // help debug
event LiquidityBridgeUpdated(address liquidityBridge);
event PegBridgeUpdated(address pegBridge);
event PegVaultUpdated(address pegVault);
event PegBridgeV2Updated(address pegBridgeV2);
event PegVaultV2Updated(address pegVaultV2);
constructor(
address _liquidityBridge,
address _pegBridge,
address _pegVault,
address _pegBridgeV2,
address _pegVaultV2
) {
liquidityBridge = _liquidityBridge;
pegBridge = _pegBridge;
pegVault = _pegVault;
pegBridgeV2 = _pegBridgeV2;
pegVaultV2 = _pegVaultV2;
}
function initReceiver(
address _liquidityBridge,
address _pegBridge,
address _pegVault,
address _pegBridgeV2,
address _pegVaultV2
) internal {
require(liquidityBridge == address(0), "liquidityBridge already set");
liquidityBridge = _liquidityBridge;
pegBridge = _pegBridge;
pegVault = _pegVault;
pegBridgeV2 = _pegBridgeV2;
pegVaultV2 = _pegVaultV2;
}
// ============== functions called by executor ==============
/**
* @notice Execute a message with a successful transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _transfer The transfer info.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessageWithTransfer(
bytes calldata _message,
MsgDataTypes.TransferInfo calldata _transfer,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) public payable {
// For message with token transfer, message Id is computed through transfer info
// in order to guarantee that each transfer can only be used once.
bytes32 messageId = verifyTransfer(_transfer);
require(executedMessages[messageId] == MsgDataTypes.TxStatus.Null, "transfer already executed");
executedMessages[messageId] = MsgDataTypes.TxStatus.Pending;
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "MessageWithTransfer"));
IBridge(liquidityBridge).verifySigs(
abi.encodePacked(domain, messageId, _message, _transfer.srcTxHash),
_sigs,
_signers,
_powers
);
MsgDataTypes.TxStatus status;
IMessageReceiverApp.ExecutionStatus est = executeMessageWithTransfer(_transfer, _message);
if (est == IMessageReceiverApp.ExecutionStatus.Success) {
status = MsgDataTypes.TxStatus.Success;
} else if (est == IMessageReceiverApp.ExecutionStatus.Retry) {
executedMessages[messageId] = MsgDataTypes.TxStatus.Null;
emit NeedRetry(
MsgDataTypes.MsgType.MessageWithTransfer,
messageId,
_transfer.srcChainId,
_transfer.srcTxHash
);
return;
} else {
est = executeMessageWithTransferFallback(_transfer, _message);
if (est == IMessageReceiverApp.ExecutionStatus.Success) {
status = MsgDataTypes.TxStatus.Fallback;
} else {
status = MsgDataTypes.TxStatus.Fail;
}
}
executedMessages[messageId] = status;
emitMessageWithTransferExecutedEvent(messageId, status, _transfer);
}
/**
* @notice Execute a message with a refunded transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _transfer The transfer info.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessageWithTransferRefund(
bytes calldata _message, // the same message associated with the original transfer
MsgDataTypes.TransferInfo calldata _transfer,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) public payable {
// similar to executeMessageWithTransfer
bytes32 messageId = verifyTransfer(_transfer);
require(executedMessages[messageId] == MsgDataTypes.TxStatus.Null, "transfer already executed");
executedMessages[messageId] = MsgDataTypes.TxStatus.Pending;
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "MessageWithTransferRefund"));
IBridge(liquidityBridge).verifySigs(
abi.encodePacked(domain, messageId, _message, _transfer.srcTxHash),
_sigs,
_signers,
_powers
);
MsgDataTypes.TxStatus status;
IMessageReceiverApp.ExecutionStatus est = executeMessageWithTransferRefund(_transfer, _message);
if (est == IMessageReceiverApp.ExecutionStatus.Success) {
status = MsgDataTypes.TxStatus.Success;
} else if (est == IMessageReceiverApp.ExecutionStatus.Retry) {
executedMessages[messageId] = MsgDataTypes.TxStatus.Null;
emit NeedRetry(
MsgDataTypes.MsgType.MessageWithTransfer,
messageId,
_transfer.srcChainId,
_transfer.srcTxHash
);
return;
} else {
status = MsgDataTypes.TxStatus.Fail;
}
executedMessages[messageId] = status;
emitMessageWithTransferExecutedEvent(messageId, status, _transfer);
}
/**
* @notice Execute a message not associated with a transfer.
* @param _message Arbitrary message bytes originated from and encoded by the source app contract
* @param _route The info about the sender and the receiver.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function executeMessage(
bytes calldata _message,
MsgDataTypes.RouteInfo calldata _route,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external payable {
MsgDataTypes.Route memory route = getRouteInfo(_route);
executeMessage(_message, route, _sigs, _signers, _powers, "Message");
}
// execute message from non-evm chain with bytes for sender address,
// otherwise same as above.
function executeMessage(
bytes calldata _message,
MsgDataTypes.RouteInfo2 calldata _route,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external payable {
MsgDataTypes.Route memory route = getRouteInfo(_route);
executeMessage(_message, route, _sigs, _signers, _powers, "Message2");
}
function executeMessage(
bytes calldata _message,
MsgDataTypes.Route memory _route,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers,
string memory domainName
) private {
// For message without associated token transfer, message Id is computed through message info,
// in order to guarantee that each message can only be applied once
bytes32 messageId = computeMessageOnlyId(_route, _message);
require(executedMessages[messageId] == MsgDataTypes.TxStatus.Null, "message already executed");
executedMessages[messageId] = MsgDataTypes.TxStatus.Pending;
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), domainName));
IBridge(liquidityBridge).verifySigs(abi.encodePacked(domain, messageId), _sigs, _signers, _powers);
MsgDataTypes.TxStatus status;
IMessageReceiverApp.ExecutionStatus est = executeMessage(_route, _message);
if (est == IMessageReceiverApp.ExecutionStatus.Success) {
status = MsgDataTypes.TxStatus.Success;
} else if (est == IMessageReceiverApp.ExecutionStatus.Retry) {
executedMessages[messageId] = MsgDataTypes.TxStatus.Null;
emit NeedRetry(MsgDataTypes.MsgType.MessageOnly, messageId, _route.srcChainId, _route.srcTxHash);
return;
} else {
status = MsgDataTypes.TxStatus.Fail;
}
executedMessages[messageId] = status;
emitMessageOnlyExecutedEvent(messageId, status, _route);
}
// ================= utils (to avoid stack too deep) =================
function emitMessageWithTransferExecutedEvent(
bytes32 _messageId,
MsgDataTypes.TxStatus _status,
MsgDataTypes.TransferInfo calldata _transfer
) private {
emit Executed(
MsgDataTypes.MsgType.MessageWithTransfer,
_messageId,
_status,
_transfer.receiver,
_transfer.srcChainId,
_transfer.srcTxHash
);
}
function emitMessageOnlyExecutedEvent(
bytes32 _messageId,
MsgDataTypes.TxStatus _status,
MsgDataTypes.Route memory _route
) private {
emit Executed(
MsgDataTypes.MsgType.MessageOnly,
_messageId,
_status,
_route.receiver,
_route.srcChainId,
_route.srcTxHash
);
}
function executeMessageWithTransfer(MsgDataTypes.TransferInfo calldata _transfer, bytes calldata _message)
private
returns (IMessageReceiverApp.ExecutionStatus)
{
uint256 gasLeftBeforeExecution = gasleft();
(bool ok, bytes memory res) = address(_transfer.receiver).call{value: msg.value}(
abi.encodeWithSelector(
IMessageReceiverApp.executeMessageWithTransfer.selector,
_transfer.sender,
_transfer.token,
_transfer.amount,
_transfer.srcChainId,
_message,
msg.sender
)
);
if (ok) {
return abi.decode((res), (IMessageReceiverApp.ExecutionStatus));
}
handleExecutionRevert(gasLeftBeforeExecution, res);
return IMessageReceiverApp.ExecutionStatus.Fail;
}
function executeMessageWithTransferFallback(MsgDataTypes.TransferInfo calldata _transfer, bytes calldata _message)
private
returns (IMessageReceiverApp.ExecutionStatus)
{
uint256 gasLeftBeforeExecution = gasleft();
(bool ok, bytes memory res) = address(_transfer.receiver).call{value: msg.value}(
abi.encodeWithSelector(
IMessageReceiverApp.executeMessageWithTransferFallback.selector,
_transfer.sender,
_transfer.token,
_transfer.amount,
_transfer.srcChainId,
_message,
msg.sender
)
);
if (ok) {
return abi.decode((res), (IMessageReceiverApp.ExecutionStatus));
}
handleExecutionRevert(gasLeftBeforeExecution, res);
return IMessageReceiverApp.ExecutionStatus.Fail;
}
function executeMessageWithTransferRefund(MsgDataTypes.TransferInfo calldata _transfer, bytes calldata _message)
private
returns (IMessageReceiverApp.ExecutionStatus)
{
uint256 gasLeftBeforeExecution = gasleft();
(bool ok, bytes memory res) = address(_transfer.receiver).call{value: msg.value}(
abi.encodeWithSelector(
IMessageReceiverApp.executeMessageWithTransferRefund.selector,
_transfer.token,
_transfer.amount,
_message,
msg.sender
)
);
if (ok) {
return abi.decode((res), (IMessageReceiverApp.ExecutionStatus));
}
handleExecutionRevert(gasLeftBeforeExecution, res);
return IMessageReceiverApp.ExecutionStatus.Fail;
}
function verifyTransfer(MsgDataTypes.TransferInfo calldata _transfer) private view returns (bytes32) {
bytes32 transferId;
address bridgeAddr;
MsgDataTypes.TransferType t = _transfer.t;
if (t == MsgDataTypes.TransferType.LqRelay) {
bridgeAddr = liquidityBridge;
transferId = keccak256(
abi.encodePacked(
_transfer.sender,
_transfer.receiver,
_transfer.token,
_transfer.amount,
_transfer.srcChainId,
uint64(block.chainid),
_transfer.refId
)
);
require(IBridge(bridgeAddr).transfers(transferId) == true, "relay not exist");
} else if (t == MsgDataTypes.TransferType.LqWithdraw) {
bridgeAddr = liquidityBridge;
transferId = keccak256(
abi.encodePacked(
uint64(block.chainid),
_transfer.wdseq,
_transfer.receiver,
_transfer.token,
_transfer.amount
)
);
require(IBridge(bridgeAddr).withdraws(transferId) == true, "withdraw not exist");
} else {
if (t == MsgDataTypes.TransferType.PegMint || t == MsgDataTypes.TransferType.PegWithdraw) {
bridgeAddr = (t == MsgDataTypes.TransferType.PegMint) ? pegBridge : pegVault;
transferId = keccak256(
abi.encodePacked(
_transfer.receiver,
_transfer.token,
_transfer.amount,
_transfer.sender,
_transfer.srcChainId,
_transfer.refId
)
);
} else {
bridgeAddr = (t == MsgDataTypes.TransferType.PegV2Mint) ? pegBridgeV2 : pegVaultV2;
transferId = keccak256(
abi.encodePacked(
_transfer.receiver,
_transfer.token,
_transfer.amount,
_transfer.sender,
_transfer.srcChainId,
_transfer.refId,
bridgeAddr
)
);
}
// function is same for peg, peg2, vault, vault2
require(IPeggedTokenBridge(bridgeAddr).records(transferId) == true, "record not exist");
}
require(IDelayedTransfer(bridgeAddr).delayedTransfers(transferId).timestamp == 0, "transfer delayed");
return keccak256(abi.encodePacked(MsgDataTypes.MsgType.MessageWithTransfer, bridgeAddr, transferId));
}
function computeMessageOnlyId(MsgDataTypes.Route memory _route, bytes calldata _message)
private
view
returns (bytes32)
{
bytes memory sender = _route.senderBytes;
if (sender.length == 0) {
sender = abi.encodePacked(_route.sender);
}
return
keccak256(
abi.encodePacked(
MsgDataTypes.MsgType.MessageOnly,
sender,
_route.receiver,
_route.srcChainId,
_route.srcTxHash,
uint64(block.chainid),
_message
)
);
}
function executeMessage(MsgDataTypes.Route memory _route, bytes calldata _message)
private
returns (IMessageReceiverApp.ExecutionStatus)
{
uint256 gasLeftBeforeExecution = gasleft();
bool ok;
bytes memory res;
if (_route.senderBytes.length == 0) {
(ok, res) = address(_route.receiver).call{value: msg.value}(
abi.encodeWithSelector(
bytes4(keccak256(bytes("executeMessage(address,uint64,bytes,address)"))),
_route.sender,
_route.srcChainId,
_message,
msg.sender
)
);
} else {
(ok, res) = address(_route.receiver).call{value: msg.value}(
abi.encodeWithSelector(
bytes4(keccak256(bytes("executeMessage(bytes,uint64,bytes,address)"))),
_route.senderBytes,
_route.srcChainId,
_message,
msg.sender
)
);
}
if (ok) {
return abi.decode((res), (IMessageReceiverApp.ExecutionStatus));
}
handleExecutionRevert(gasLeftBeforeExecution, res);
return IMessageReceiverApp.ExecutionStatus.Fail;
}
function handleExecutionRevert(uint256 _gasLeftBeforeExecution, bytes memory _returnData) private {
uint256 gasLeftAfterExecution = gasleft();
uint256 maxTargetGasLimit = block.gaslimit - preExecuteMessageGasUsage;
if (_gasLeftBeforeExecution < maxTargetGasLimit && gasLeftAfterExecution <= _gasLeftBeforeExecution / 64) {
// if this happens, the executor must have not provided sufficient gas limit,
// then the tx should revert instead of recording a non-retryable failure status
// https://github.com/wolflo/evm-opcodes/blob/main/gas.md#aa-f-gas-to-send-with-call-operations
assembly {
invalid()
}
}
string memory revertMsg = Utils.getRevertMsg(_returnData);
// revert the execution if the revert message has the ABORT prefix
checkAbortPrefix(revertMsg);
// otherwiase, emit revert message, return and mark the execution as failed (non-retryable)
emit CallReverted(revertMsg);
}
function checkAbortPrefix(string memory _revertMsg) private pure {
bytes memory prefixBytes = bytes(MsgDataTypes.ABORT_PREFIX);
bytes memory msgBytes = bytes(_revertMsg);
if (msgBytes.length >= prefixBytes.length) {
for (uint256 i = 0; i < prefixBytes.length; i++) {
if (msgBytes[i] != prefixBytes[i]) {
return; // prefix not match, return
}
}
revert(_revertMsg); // prefix match, revert
}
}
function getRouteInfo(MsgDataTypes.RouteInfo calldata _route) private pure returns (MsgDataTypes.Route memory) {
return MsgDataTypes.Route(_route.sender, "", _route.receiver, _route.srcChainId, _route.srcTxHash);
}
function getRouteInfo(MsgDataTypes.RouteInfo2 calldata _route) private pure returns (MsgDataTypes.Route memory) {
return MsgDataTypes.Route(address(0), _route.sender, _route.receiver, _route.srcChainId, _route.srcTxHash);
}
// ================= helper functions =====================
/**
* @notice combine bridge transfer and msg execution calls into a single tx
* @dev caller needs to get the required input params from SGN
* @param _tp params to call bridge transfer
* @param _mp params to execute message
*/
function transferAndExecuteMsg(
MsgDataTypes.BridgeTransferParams calldata _tp,
MsgDataTypes.MsgWithTransferExecutionParams calldata _mp
) external {
_bridgeTransfer(_mp.transfer.t, _tp);
executeMessageWithTransfer(_mp.message, _mp.transfer, _mp.sigs, _mp.signers, _mp.powers);
}
/**
* @notice combine bridge refund and msg execution calls into a single tx
* @dev caller needs to get the required input params from SGN
* @param _tp params to call bridge transfer for refund
* @param _mp params to execute message for refund
*/
function refundAndExecuteMsg(
MsgDataTypes.BridgeTransferParams calldata _tp,
MsgDataTypes.MsgWithTransferExecutionParams calldata _mp
) external {
_bridgeTransfer(_mp.transfer.t, _tp);
executeMessageWithTransferRefund(_mp.message, _mp.transfer, _mp.sigs, _mp.signers, _mp.powers);
}
function _bridgeTransfer(MsgDataTypes.TransferType t, MsgDataTypes.BridgeTransferParams calldata _params) private {
if (t == MsgDataTypes.TransferType.LqRelay) {
IBridge(liquidityBridge).relay(_params.request, _params.sigs, _params.signers, _params.powers);
} else if (t == MsgDataTypes.TransferType.LqWithdraw) {
IBridge(liquidityBridge).withdraw(_params.request, _params.sigs, _params.signers, _params.powers);
} else if (t == MsgDataTypes.TransferType.PegMint) {
IPeggedTokenBridge(pegBridge).mint(_params.request, _params.sigs, _params.signers, _params.powers);
} else if (t == MsgDataTypes.TransferType.PegV2Mint) {
IPeggedTokenBridgeV2(pegBridgeV2).mint(_params.request, _params.sigs, _params.signers, _params.powers);
} else if (t == MsgDataTypes.TransferType.PegWithdraw) {
IOriginalTokenVault(pegVault).withdraw(_params.request, _params.sigs, _params.signers, _params.powers);
} else if (t == MsgDataTypes.TransferType.PegV2Withdraw) {
IOriginalTokenVaultV2(pegVaultV2).withdraw(_params.request, _params.sigs, _params.signers, _params.powers);
}
}
// ================= contract config =================
function setLiquidityBridge(address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
liquidityBridge = _addr;
emit LiquidityBridgeUpdated(liquidityBridge);
}
function setPegBridge(address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
pegBridge = _addr;
emit PegBridgeUpdated(pegBridge);
}
function setPegVault(address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
pegVault = _addr;
emit PegVaultUpdated(pegVault);
}
function setPegBridgeV2(address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
pegBridgeV2 = _addr;
emit PegBridgeV2Updated(pegBridgeV2);
}
function setPegVaultV2(address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
pegVaultV2 = _addr;
emit PegVaultV2Updated(pegVaultV2);
}
function setPreExecuteMessageGasUsage(uint256 _usage) public onlyOwner {
preExecuteMessageGasUsage = _usage;
}
}
================================================
FILE: contracts/message/messagebus/MessageBusSender.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../../safeguard/Ownable.sol";
import "../../interfaces/ISigsVerifier.sol";
contract MessageBusSender is Ownable {
ISigsVerifier public immutable sigsVerifier;
uint256 public feeBase;
uint256 public feePerByte;
mapping(address => uint256) public withdrawnFees;
event Message(address indexed sender, address receiver, uint256 dstChainId, bytes message, uint256 fee);
// message to non-evm chain with >20 bytes addr
event Message2(address indexed sender, bytes receiver, uint256 dstChainId, bytes message, uint256 fee);
event MessageWithTransfer(
address indexed sender,
address receiver,
uint256 dstChainId,
address bridge,
bytes32 srcTransferId,
bytes message,
uint256 fee
);
event FeeWithdrawn(address receiver, uint256 amount);
event FeeBaseUpdated(uint256 feeBase);
event FeePerByteUpdated(uint256 feePerByte);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Sends a message to a contract on another chain.
* Sender needs to make sure the uniqueness of the message Id, which is computed as
* hash(type.MessageOnly, sender, receiver, srcChainId, srcTxHash, dstChainId, message).
* If messages with the same Id are sent, only one of them will succeed at dst chain.
* A fee is charged in the native gas token.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
*/
function sendMessage(
address _receiver,
uint256 _dstChainId,
bytes calldata _message
) external payable {
_sendMessage(_dstChainId, _message);
emit Message(msg.sender, _receiver, _dstChainId, _message, msg.value);
}
// Send message to non-evm chain with bytes for receiver address,
// otherwise same as above.
function sendMessage(
bytes calldata _receiver,
uint256 _dstChainId,
bytes calldata _message
) external payable {
_sendMessage(_dstChainId, _message);
emit Message2(msg.sender, _receiver, _dstChainId, _message, msg.value);
}
function _sendMessage(uint256 _dstChainId, bytes calldata _message) private {
require(_dstChainId != block.chainid, "Invalid chainId");
uint256 minFee = calcFee(_message);
require(msg.value >= minFee, "Insufficient fee");
}
/**
* @notice Sends a message associated with a transfer to a contract on another chain.
* If messages with the same srcTransferId are sent, only one of them will succeed.
* A fee is charged in the native token.
* @param _receiver The address of the destination app contract.
* @param _dstChainId The destination chain ID.
* @param _srcBridge The bridge contract to send the transfer with.
* @param _srcTransferId The transfer ID.
* @param _dstChainId The destination chain ID.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
*/
function sendMessageWithTransfer(
address _receiver,
uint256 _dstChainId,
address _srcBridge,
bytes32 _srcTransferId,
bytes calldata _message
) external payable {
require(_dstChainId != block.chainid, "Invalid chainId");
uint256 minFee = calcFee(_message);
require(msg.value >= minFee, "Insufficient fee");
// SGN needs to verify
// 1. msg.sender matches sender of the src transfer
// 2. dstChainId matches dstChainId of the src transfer
// 3. bridge is either liquidity bridge, peg src vault, or peg dst bridge
emit MessageWithTransfer(msg.sender, _receiver, _dstChainId, _srcBridge, _srcTransferId, _message, msg.value);
}
/**
* @notice Withdraws message fee in the form of native gas token.
* @param _account The address receiving the fee.
* @param _cumulativeFee The cumulative fee credited to the account. Tracked by SGN.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A withdrawal must be
* signed-off by +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdrawFee(
address _account,
uint256 _cumulativeFee,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "withdrawFee"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _account, _cumulativeFee), _sigs, _signers, _powers);
uint256 amount = _cumulativeFee - withdrawnFees[_account];
require(amount > 0, "No new amount to withdraw");
withdrawnFees[_account] = _cumulativeFee;
(bool sent, ) = _account.call{value: amount, gas: 50000}("");
require(sent, "failed to withdraw fee");
emit FeeWithdrawn(_account, amount);
}
/**
* @notice Calculates the required fee for the message.
* @param _message Arbitrary message bytes to be decoded by the destination app contract.
@ @return The required fee.
*/
function calcFee(bytes calldata _message) public view returns (uint256) {
return feeBase + _message.length * feePerByte;
}
// -------------------- Admin --------------------
function setFeePerByte(uint256 _fee) external onlyOwner {
feePerByte = _fee;
emit FeePerByteUpdated(feePerByte);
}
function setFeeBase(uint256 _fee) external onlyOwner {
feeBase = _fee;
emit FeeBaseUpdated(feeBase);
}
}
================================================
FILE: contracts/message/safeguard/DelayedMessage.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "../../safeguard/Ownable.sol";
abstract contract DelayedMessage is Ownable {
// universal unique id (not msgId) => delay start time
mapping(bytes32 => uint256) public delayedMessages;
uint256 public delayPeriod; // in seconds
uint32 public nonce;
event DelayedMessageAdded(bytes32 id, address srcContract, uint64 srcChainId, bytes message, uint32 nonce);
event DelayedMessageExecuted(bytes32 id);
event DelayPeriodUpdated(uint256 period);
function _addDelayedMessage(
address _srcContract,
uint64 _srcChainId,
bytes calldata _message
) internal {
bytes32 id = keccak256(abi.encodePacked(_srcContract, _srcChainId, _message, uint64(block.chainid), nonce));
delayedMessages[id] = uint256(block.timestamp);
emit DelayedMessageAdded(id, _srcContract, _srcChainId, _message, nonce);
nonce += 1;
}
// caller needs to do the actual message execution
function _executeDelayedMessage(
address _srcContract,
uint64 _srcChainId,
bytes memory _message,
uint32 _nonce
) internal {
bytes32 id = keccak256(abi.encodePacked(_srcContract, _srcChainId, _message, uint64(block.chainid), _nonce));
require(delayedMessages[id] > 0, "delayed message not exist");
require(block.timestamp > delayedMessages[id] + delayPeriod, "delayed message still locked");
delete delayedMessages[id];
emit DelayedMessageExecuted(id);
}
function setDelayPeriod(uint256 _period) external onlyOwner {
delayPeriod = _period;
emit DelayPeriodUpdated(_period);
}
}
================================================
FILE: contracts/message/safeguard/MessageAppPauser.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
import "../libraries/MsgDataTypes.sol";
import "../../safeguard/Pauser.sol";
abstract contract MessageAppPauser is Pauser {
/**
* @dev Modifier to make the message execution function callable only when
* the contract is not paused.
*
* Added the ABORT_PREFIX ("MSG::ABORT:") in front of the revert message to
* work with the Celer IM MessageBus contract, so that the message execution
* can be retried later when the contract is unpaused.
*/
modifier whenNotMsgPaused() {
require(!paused(), MsgDataTypes.abortReason("Pausable: paused"));
_;
}
}
================================================
FILE: contracts/miscs/Faucet.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract Faucet is Ownable {
using SafeERC20 for IERC20;
uint256 public minDripBlkInterval;
mapping(address => uint256) public lastDripBlk;
/**
* @dev Sends 0.01% of each token to the caller.
* @param tokens The tokens to drip.
*/
function drip(address[] calldata tokens) public {
require(block.number - lastDripBlk[msg.sender] >= minDripBlkInterval, "too frequent");
for (uint256 i = 0; i < tokens.length; i++) {
IERC20 token = IERC20(tokens[i]);
uint256 balance = token.balanceOf(address(this));
require(balance > 0, "Faucet is empty");
token.safeTransfer(msg.sender, balance / 10000); // 0.01%
}
lastDripBlk[msg.sender] = block.number;
}
/**
* @dev Owner set minDripBlkInterval
*
* @param _interval minDripBlkInterval value
*/
function setMinDripBlkInterval(uint256 _interval) external onlyOwner {
minDripBlkInterval = _interval;
}
/**
* @dev Owner drains one type of tokens
*
* @param _asset drained asset address
* @param _amount drained asset amount
*/
function drainToken(address _asset, uint256 _amount) external onlyOwner {
IERC20(_asset).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/miscs/MintableERC20.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
/**
* @title A mintable {ERC20} token.
*/
contract MintableERC20 is ERC20Burnable, Ownable {
uint8 private _decimals;
/**
* @dev Constructor that gives msg.sender an initial supply of tokens.
*/
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
uint256 initialSupply_
) ERC20(name_, symbol_) {
_decimals = decimals_;
_mint(msg.sender, initialSupply_);
}
/**
* @dev Creates `amount` new tokens for `to`.
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
================================================
FILE: contracts/miscs/oasys/L1StandardERC20.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/utils/Context.sol";
/**
* @dev {ERC20} token, including:
*
* - ability for holders to burn (destroy) their tokens
* - a minter role that allows for token minting (creation)
* - a pauser role that allows to stop all token transfers
*
* This contract uses {AccessControl} to lock permissioned functions using the
* different roles - head to its documentation for details.
*
* The account that deploys the contract will be granted the minter and pauser
* roles, as well as the default admin role, which will let it grant both minter
* and pauser roles to other accounts.
*/
contract L1StandardERC20 is Context, AccessControlEnumerable, ERC20Burnable, ERC20Pausable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
/**
* @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the
* account that deploys the contract.
*
* See {ERC20-constructor}.
*/
constructor(
address owner,
string memory name,
string memory symbol
) ERC20(name, symbol) {
_setupRole(DEFAULT_ADMIN_ROLE, owner);
_setupRole(MINTER_ROLE, owner);
_setupRole(PAUSER_ROLE, owner);
}
/**
* @dev Creates `amount` new tokens for `to`.
*
* See {ERC20-_mint}.
*
* Requirements:
*
* - the caller must have the `MINTER_ROLE`.
*/
function mint(address to, uint256 amount) public virtual {
require(hasRole(MINTER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have minter role to mint");
_mint(to, amount);
}
/**
* @dev Pauses all token transfers.
*
* See {ERC20Pausable} and {Pausable-_pause}.
*
* Requirements:
*
* - the caller must have the `PAUSER_ROLE`.
*/
function pause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to pause");
_pause();
}
/**
* @dev Unpauses all token transfers.
*
* See {ERC20Pausable} and {Pausable-_unpause}.
*
* Requirements:
*
* - the caller must have the `PAUSER_ROLE`.
*/
function unpause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to unpause");
_unpause();
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override(ERC20, ERC20Pausable) {
super._beforeTokenTransfer(from, to, amount);
}
}
================================================
FILE: contracts/miscs/oasys/L1StandardERC20Factory.sol
================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import {L1StandardERC20} from "./L1StandardERC20.sol";
/**
* @title L1StandardERC20Factory
* @dev L1StandardERC20Factory deploys the Oasys Standard ERC20 contract.
*/
contract L1StandardERC20Factory {
/**********
* Events *
**********/
event ERC20Created(string indexed _symbol, address indexed _address);
/********************
* Public Functions *
********************/
/**
* Deploys the Oasys Standard ERC20.
* @param _name Name of the ERC20.
* @param _symbol Symbol of the ERC20.
*/
function createStandardERC20(string memory _name, string memory _symbol) external {
L1StandardERC20 erc20 = new L1StandardERC20(msg.sender, _name, _symbol);
emit ERC20Created(_symbol, address(erc20));
}
}
================================================
FILE: contracts/miscs/oasys/README.md
================================================
# Oasys specific contracts
Used for generating typechain for oasys_token_factory.ts to deploy new tokens on Oasys chain
================================================
FILE: contracts/miscs/proxy-admin/Ownable.sol
================================================
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (access/Ownable.sol)
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor (address initialOwner) {
_transferOwnership(initialOwner);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
================================================
FILE: contracts/miscs/proxy-admin/ProxyAdmin.sol
================================================
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol)
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "./Ownable.sol";
/**
* @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an
* explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}.
*/
contract ProxyAdmin is Ownable {
constructor (address initialOwner) Ownable(initialOwner) {}
/**
* @dev Returns the current implementation of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Returns the current admin of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Changes the admin of `proxy` to `newAdmin`.
*
* Requirements:
*
* - This contract must be the current admin of `proxy`.
*/
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}
/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}
/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
================================================
FILE: contracts/pegged-bridge/OriginalTokenVault.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/ISigsVerifier.sol";
import "../interfaces/IWETH.sol";
import "../libraries/PbPegged.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
/**
* @title the vault to deposit and withdraw original tokens
* @dev Work together with PeggedTokenBridge contracts deployed at remote chains
*/
contract OriginalTokenVault is ReentrancyGuard, Pauser, VolumeControl, DelayedTransfer {
using SafeERC20 for IERC20;
ISigsVerifier public immutable sigsVerifier;
mapping(bytes32 => bool) public records;
mapping(address => uint256) public minDeposit;
mapping(address => uint256) public maxDeposit;
address public nativeWrap;
uint256 public nativeTokenTransferGas = 50000;
event Deposited(
bytes32 depositId,
address depositor,
address token,
uint256 amount,
uint64 mintChainId,
address mintAccount
);
event Withdrawn(
bytes32 withdrawId,
address receiver,
address token,
uint256 amount,
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case of burn-withdraw: the chain ID on which the corresponding burn happened;
// 2. Pegbridge fee claim: zero / not applicable;
// 3. Refund for wrong deposit: this chain ID on which the deposit happened
uint64 refChainId,
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of burn-withdraw: the burn ID on the remote chain;
// 2. Pegbridge fee claim: a per-account nonce;
// 3. Refund for wrong deposit: the deposit ID on this chain
bytes32 refId,
address burnAccount
);
event MinDepositUpdated(address token, uint256 amount);
event MaxDepositUpdated(address token, uint256 amount);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Lock original tokens to trigger cross-chain mint of pegged tokens at a remote chain's PeggedTokenBridge.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The original token address.
* @param _amount The amount to deposit.
* @param _mintChainId The destination chain ID to mint tokens.
* @param _mintAccount The destination account to receive the minted pegged tokens.
* @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice.
*/
function deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external nonReentrant whenNotPaused {
bytes32 depId = _deposit(_token, _amount, _mintChainId, _mintAccount, _nonce);
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposited(depId, msg.sender, _token, _amount, _mintChainId, _mintAccount);
}
/**
* @notice Lock native token as original token to trigger cross-chain mint of pegged tokens at a remote chain's
* PeggedTokenBridge.
* @param _amount The amount to deposit.
* @param _mintChainId The destination chain ID to mint tokens.
* @param _mintAccount The destination account to receive the minted pegged tokens.
* @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice.
*/
function depositNative(
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external payable nonReentrant whenNotPaused {
require(msg.value == _amount, "Amount mismatch");
require(nativeWrap != address(0), "Native wrap not set");
bytes32 depId = _deposit(nativeWrap, _amount, _mintChainId, _mintAccount, _nonce);
IWETH(nativeWrap).deposit{value: _amount}();
emit Deposited(depId, msg.sender, nativeWrap, _amount, _mintChainId, _mintAccount);
}
function _deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) private returns (bytes32) {
require(_amount > minDeposit[_token], "amount too small");
require(maxDeposit[_token] == 0 || _amount <= maxDeposit[_token], "amount too large");
bytes32 depId = keccak256(
// len = 20 + 20 + 32 + 8 + 20 + 8 + 8 = 116
abi.encodePacked(msg.sender, _token, _amount, _mintChainId, _mintAccount, _nonce, uint64(block.chainid))
);
require(records[depId] == false, "record exists");
records[depId] = true;
return depId;
}
/**
* @notice Withdraw locked original tokens triggered by a burn at a remote chain's PeggedTokenBridge.
* @param _request The serialized Withdraw protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Withdraw"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _request), _sigs, _signers, _powers);
PbPegged.Withdraw memory request = PbPegged.decWithdraw(_request);
bytes32 wdId = keccak256(
// len = 20 + 20 + 32 + 20 + 8 + 32 = 132
abi.encodePacked(
request.receiver,
request.token,
request.amount,
request.burnAccount,
request.refChainId,
request.refId
)
);
require(records[wdId] == false, "record exists");
records[wdId] = true;
_updateVolume(request.token, request.amount);
uint256 delayThreshold = delayThresholds[request.token];
if (delayThreshold > 0 && request.amount > delayThreshold) {
_addDelayedTransfer(wdId, request.receiver, request.token, request.amount);
} else {
_sendToken(request.receiver, request.token, request.amount);
}
emit Withdrawn(
wdId,
request.receiver,
request.token,
request.amount,
request.refChainId,
request.refId,
request.burnAccount
);
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
_sendToken(transfer.receiver, transfer.token, transfer.amount);
}
function setMinDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minDeposit[_tokens[i]] = _amounts[i];
emit MinDepositUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxDeposit[_tokens[i]] = _amounts[i];
emit MaxDepositUpdated(_tokens[i], _amounts[i]);
}
}
function setWrap(address _weth) external onlyOwner {
nativeWrap = _weth;
}
function _sendToken(
address _receiver,
address _token,
uint256 _amount
) private {
if (_token == nativeWrap) {
// withdraw then transfer native to receiver
IWETH(nativeWrap).withdraw(_amount);
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyGovernor {
nativeTokenTransferGas = _gasUsed;
}
receive() external payable {}
}
================================================
FILE: contracts/pegged-bridge/OriginalTokenVaultV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/ISigsVerifier.sol";
import "../interfaces/IWETH.sol";
import "../libraries/PbPegged.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
/**
* @title the vault to deposit and withdraw original tokens
* @dev Work together with PeggedTokenBridge contracts deployed at remote chains
*/
contract OriginalTokenVaultV2 is ReentrancyGuard, Pauser, VolumeControl, DelayedTransfer {
using SafeERC20 for IERC20;
ISigsVerifier public immutable sigsVerifier;
mapping(bytes32 => bool) public records;
mapping(address => uint256) public minDeposit;
mapping(address => uint256) public maxDeposit;
address public nativeWrap;
uint256 public nativeTokenTransferGas = 50000;
event Deposited(
bytes32 depositId,
address depositor,
address token,
uint256 amount,
uint64 mintChainId,
address mintAccount,
uint64 nonce
);
event Withdrawn(
bytes32 withdrawId,
address receiver,
address token,
uint256 amount,
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case of burn-withdraw: the chain ID on which the corresponding burn happened;
// 2. Pegbridge fee claim: zero / not applicable;
// 3. Refund for wrong deposit: this chain ID on which the deposit happened
uint64 refChainId,
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of burn-withdraw: the burn ID on the remote chain;
// 2. Pegbridge fee claim: a per-account nonce;
// 3. Refund for wrong deposit: the deposit ID on this chain
bytes32 refId,
address burnAccount
);
event MinDepositUpdated(address token, uint256 amount);
event MaxDepositUpdated(address token, uint256 amount);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Lock original tokens to trigger cross-chain mint of pegged tokens at a remote chain's PeggedTokenBridge.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The original token address.
* @param _amount The amount to deposit.
* @param _mintChainId The destination chain ID to mint tokens.
* @param _mintAccount The destination account to receive the minted pegged tokens.
* @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice.
*/
function deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external nonReentrant whenNotPaused returns (bytes32) {
bytes32 depId = _deposit(_token, _amount, _mintChainId, _mintAccount, _nonce);
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposited(depId, msg.sender, _token, _amount, _mintChainId, _mintAccount, _nonce);
return depId;
}
/**
* @notice Lock native token as original token to trigger cross-chain mint of pegged tokens at a remote chain's
* PeggedTokenBridge.
* @param _amount The amount to deposit.
* @param _mintChainId The destination chain ID to mint tokens.
* @param _mintAccount The destination account to receive the minted pegged tokens.
* @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice.
*/
function depositNative(
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) external payable nonReentrant whenNotPaused returns (bytes32) {
require(msg.value == _amount, "Amount mismatch");
require(nativeWrap != address(0), "Native wrap not set");
bytes32 depId = _deposit(nativeWrap, _amount, _mintChainId, _mintAccount, _nonce);
IWETH(nativeWrap).deposit{value: _amount}();
emit Deposited(depId, msg.sender, nativeWrap, _amount, _mintChainId, _mintAccount, _nonce);
return depId;
}
function _deposit(
address _token,
uint256 _amount,
uint64 _mintChainId,
address _mintAccount,
uint64 _nonce
) private returns (bytes32) {
require(_amount > minDeposit[_token], "amount too small");
require(maxDeposit[_token] == 0 || _amount <= maxDeposit[_token], "amount too large");
bytes32 depId = keccak256(
// len = 20 + 20 + 32 + 8 + 20 + 8 + 8 + 20 = 136
abi.encodePacked(
msg.sender,
_token,
_amount,
_mintChainId,
_mintAccount,
_nonce,
uint64(block.chainid),
address(this)
)
);
require(records[depId] == false, "record exists");
records[depId] = true;
return depId;
}
/**
* @notice Withdraw locked original tokens triggered by a burn at a remote chain's PeggedTokenBridge.
* @param _request The serialized Withdraw protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the bridge's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function withdraw(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused returns (bytes32) {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Withdraw"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _request), _sigs, _signers, _powers);
PbPegged.Withdraw memory request = PbPegged.decWithdraw(_request);
bytes32 wdId = keccak256(
// len = 20 + 20 + 32 + 20 + 8 + 32 + 20 = 152
abi.encodePacked(
request.receiver,
request.token,
request.amount,
request.burnAccount,
request.refChainId,
request.refId,
address(this)
)
);
require(records[wdId] == false, "record exists");
records[wdId] = true;
_updateVolume(request.token, request.amount);
uint256 delayThreshold = delayThresholds[request.token];
if (delayThreshold > 0 && request.amount > delayThreshold) {
_addDelayedTransfer(wdId, request.receiver, request.token, request.amount);
} else {
_sendToken(request.receiver, request.token, request.amount);
}
emit Withdrawn(
wdId,
request.receiver,
request.token,
request.amount,
request.refChainId,
request.refId,
request.burnAccount
);
return wdId;
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
_sendToken(transfer.receiver, transfer.token, transfer.amount);
}
function setMinDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minDeposit[_tokens[i]] = _amounts[i];
emit MinDepositUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxDeposit[_tokens[i]] = _amounts[i];
emit MaxDepositUpdated(_tokens[i], _amounts[i]);
}
}
function setWrap(address _weth) external onlyOwner {
nativeWrap = _weth;
}
function _sendToken(
address _receiver,
address _token,
uint256 _amount
) private {
if (_token == nativeWrap) {
// withdraw then transfer native to receiver
IWETH(nativeWrap).withdraw(_amount);
(bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}("");
require(sent, "failed to send native token");
} else {
IERC20(_token).safeTransfer(_receiver, _amount);
}
}
function setNativeTokenTransferGas(uint256 _gasUsed) external onlyGovernor {
nativeTokenTransferGas = _gasUsed;
}
receive() external payable {}
}
================================================
FILE: contracts/pegged-bridge/PeggedBrc20Bridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../interfaces/IPeggedToken.sol";
import "../interfaces/IPeggedTokenBurnFrom.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
/**
* @title The bridge contract to mint and burn pegged brc20 tokens
*/
contract PeggedBrc20Bridge is Pauser, VolumeControl, DelayedTransfer {
address public minter;
mapping(bytes32 => bool) public records;
mapping(address => uint256) public supplies;
mapping(address => uint256) public minBurn;
mapping(address => uint256) public maxBurn;
event Mint(
bytes32 mintId,
address token,
address account,
uint256 amount,
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case: the chain ID on which the remote corresponding deposit or burn happened;
// 2. Refund for wrong burn: this chain ID on which the burn happened
uint64 refChainId,
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of deposit/burn-mint: the deposit or burn ID on the remote chain;
// 2. Refund for wrong burn: the burn ID on this chain
bytes32 refId,
bytes depositor
);
event Burn(
bytes32 burnId,
address token,
address account,
uint256 amount,
uint64 toChainId,
bytes toAccount,
uint64 nonce
);
event MinBurnUpdated(address token, uint256 amount);
event MaxBurnUpdated(address token, uint256 amount);
event SupplyUpdated(address token, uint256 supply);
event MinterUpdated(address origMinter, address newMinter);
constructor(address _minter) {
minter = _minter;
}
/**
* @notice Mint tokens triggered by deposit on BTC chain
* @param _receiver The receiver address.
* @param _token The pegged token.
* @param _amount The amount.
* @param _depositor The depositor BTC address.
* @param _refChainId The BTC chain ID.
* @param _refId Reserved reference ID.
*/
function mint(
address _receiver,
address _token,
uint256 _amount,
bytes calldata _depositor,
uint64 _refChainId,
bytes32 _refId
) external whenNotPaused returns (bytes32) {
require(msg.sender == minter, "not minter");
bytes32 mintId = keccak256(
abi.encodePacked(_receiver, _token, _amount, _depositor, _refChainId, _refId, address(this))
);
require(records[mintId] == false, "record exists");
records[mintId] = true;
_updateVolume(_token, _amount);
uint256 delayThreshold = delayThresholds[_token];
if (delayThreshold > 0 && _amount > delayThreshold) {
_addDelayedTransfer(mintId, _receiver, _token, _amount);
} else {
IPeggedToken(_token).mint(_receiver, _amount);
}
supplies[_token] += _amount;
emit Mint(mintId, _token, _receiver, _amount, _refChainId, _refId, _depositor);
return mintId;
}
/**
* @notice Burn pegged tokens to trigger a cross-chain withdrawal of the original tokens on the BTC chain.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The pegged token address.
* @param _amount The amount to burn.
* @param _toChainId If zero, withdraw from BTC chain; otherwise, the remote chain to mint tokens.
* @param _toAccount The account to receive tokens on the remote chain
* @param _nonce A number to guarantee unique depositId. Can be timestamp in practice.
*/
function burn(
address _token,
uint256 _amount,
uint64 _toChainId,
bytes calldata _toAccount,
uint64 _nonce
) external whenNotPaused returns (bytes32) {
bytes32 burnId = _burn(_token, _amount, _toChainId, _toAccount, _nonce);
IPeggedToken(_token).burn(msg.sender, _amount);
return burnId;
}
// same with `burn` above, use openzeppelin ERC20Burnable interface
function burnFrom(
address _token,
uint256 _amount,
uint64 _toChainId,
bytes calldata _toAccount,
uint64 _nonce
) external whenNotPaused returns (bytes32) {
bytes32 burnId = _burn(_token, _amount, _toChainId, _toAccount, _nonce);
IPeggedTokenBurnFrom(_token).burnFrom(msg.sender, _amount);
return burnId;
}
function _burn(
address _token,
uint256 _amount,
uint64 _toChainId,
bytes calldata _toAccount,
uint64 _nonce
) internal returns (bytes32) {
require(_amount > minBurn[_token], "amount too small");
require(maxBurn[_token] == 0 || _amount <= maxBurn[_token], "amount too large");
supplies[_token] -= _amount;
bytes32 burnId = keccak256(
abi.encodePacked(
msg.sender,
_token,
_amount,
_toChainId,
_toAccount,
_nonce,
uint64(block.chainid),
address(this)
)
);
require(records[burnId] == false, "record exists");
records[burnId] = true;
emit Burn(burnId, _token, msg.sender, _amount, _toChainId, _toAccount, _nonce);
return burnId;
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
IPeggedToken(transfer.token).mint(transfer.receiver, transfer.amount);
}
function setMinBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minBurn[_tokens[i]] = _amounts[i];
emit MinBurnUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxBurn[_tokens[i]] = _amounts[i];
emit MaxBurnUpdated(_tokens[i], _amounts[i]);
}
}
function setSupply(address _token, uint256 _supply) external onlyOwner {
supplies[_token] = _supply;
emit SupplyUpdated(_token, _supply);
}
function increaseSupply(address _token, uint256 _delta) external onlyOwner {
supplies[_token] += _delta;
emit SupplyUpdated(_token, supplies[_token]);
}
function decreaseSupply(address _token, uint256 _delta) external onlyOwner {
supplies[_token] -= _delta;
emit SupplyUpdated(_token, supplies[_token]);
}
function setMinter(address _minter) external onlyOwner {
address origMinter = minter;
minter = _minter;
emit MinterUpdated(origMinter, _minter);
}
}
================================================
FILE: contracts/pegged-bridge/PeggedTokenBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../interfaces/ISigsVerifier.sol";
import "../interfaces/IPeggedToken.sol";
import "../libraries/PbPegged.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
/**
* @title The bridge contract to mint and burn pegged tokens
* @dev Work together with OriginalTokenVault deployed at remote chains.
*/
contract PeggedTokenBridge is Pauser, VolumeControl, DelayedTransfer {
ISigsVerifier public immutable sigsVerifier;
mapping(bytes32 => bool) public records;
mapping(address => uint256) public minBurn;
mapping(address => uint256) public maxBurn;
event Mint(
bytes32 mintId,
address token,
address account,
uint256 amount,
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case: the chain ID on which the remote corresponding deposit or burn happened;
// 2. Refund for wrong burn: this chain ID on which the burn happened
uint64 refChainId,
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of deposit/burn-mint: the deposit or burn ID on the remote chain;
// 2. Refund for wrong burn: the burn ID on this chain
bytes32 refId,
address depositor
);
event Burn(bytes32 burnId, address token, address account, uint256 amount, address withdrawAccount);
event MinBurnUpdated(address token, uint256 amount);
event MaxBurnUpdated(address token, uint256 amount);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Mint tokens triggered by deposit at a remote chain's OriginalTokenVault.
* @param _request The serialized Mint protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function mint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Mint"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _request), _sigs, _signers, _powers);
PbPegged.Mint memory request = PbPegged.decMint(_request);
bytes32 mintId = keccak256(
// len = 20 + 20 + 32 + 20 + 8 + 32 = 132
abi.encodePacked(
request.account,
request.token,
request.amount,
request.depositor,
request.refChainId,
request.refId
)
);
require(records[mintId] == false, "record exists");
records[mintId] = true;
_updateVolume(request.token, request.amount);
uint256 delayThreshold = delayThresholds[request.token];
if (delayThreshold > 0 && request.amount > delayThreshold) {
_addDelayedTransfer(mintId, request.account, request.token, request.amount);
} else {
IPeggedToken(request.token).mint(request.account, request.amount);
}
emit Mint(
mintId,
request.token,
request.account,
request.amount,
request.refChainId,
request.refId,
request.depositor
);
}
/**
* @notice Burn pegged tokens to trigger a cross-chain withdrawal of the original tokens at a remote chain's
* OriginalTokenVault.
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The pegged token address.
* @param _amount The amount to burn.
* @param _withdrawAccount The account to receive the original tokens withdrawn on the remote chain.
* @param _nonce A number to guarantee unique depositId. Can be timestamp in practice.
*/
function burn(
address _token,
uint256 _amount,
address _withdrawAccount,
uint64 _nonce
) external whenNotPaused {
require(_amount > minBurn[_token], "amount too small");
require(maxBurn[_token] == 0 || _amount <= maxBurn[_token], "amount too large");
bytes32 burnId = keccak256(
// len = 20 + 20 + 32 + 20 + 8 + 8 = 108
abi.encodePacked(msg.sender, _token, _amount, _withdrawAccount, _nonce, uint64(block.chainid))
);
require(records[burnId] == false, "record exists");
records[burnId] = true;
IPeggedToken(_token).burn(msg.sender, _amount);
emit Burn(burnId, _token, msg.sender, _amount, _withdrawAccount);
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
IPeggedToken(transfer.token).mint(transfer.receiver, transfer.amount);
}
function setMinBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minBurn[_tokens[i]] = _amounts[i];
emit MinBurnUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxBurn[_tokens[i]] = _amounts[i];
emit MaxBurnUpdated(_tokens[i], _amounts[i]);
}
}
}
================================================
FILE: contracts/pegged-bridge/PeggedTokenBridgeV2.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../interfaces/ISigsVerifier.sol";
import "../interfaces/IPeggedToken.sol";
import "../interfaces/IPeggedTokenBurnFrom.sol";
import "../libraries/PbPegged.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/VolumeControl.sol";
import "../safeguard/DelayedTransfer.sol";
/**
* @title The bridge contract to mint and burn pegged tokens
* @dev Work together with OriginalTokenVault deployed at remote chains.
*/
contract PeggedTokenBridgeV2 is Pauser, VolumeControl, DelayedTransfer {
ISigsVerifier public immutable sigsVerifier;
mapping(bytes32 => bool) public records;
mapping(address => uint256) public supplies;
mapping(address => uint256) public minBurn;
mapping(address => uint256) public maxBurn;
event Mint(
bytes32 mintId,
address token,
address account,
uint256 amount,
// ref_chain_id defines the reference chain ID, taking values of:
// 1. The common case: the chain ID on which the remote corresponding deposit or burn happened;
// 2. Refund for wrong burn: this chain ID on which the burn happened
uint64 refChainId,
// ref_id defines a unique reference ID, taking values of:
// 1. The common case of deposit/burn-mint: the deposit or burn ID on the remote chain;
// 2. Refund for wrong burn: the burn ID on this chain
bytes32 refId,
address depositor
);
event Burn(
bytes32 burnId,
address token,
address account,
uint256 amount,
uint64 toChainId,
address toAccount,
uint64 nonce
);
event MinBurnUpdated(address token, uint256 amount);
event MaxBurnUpdated(address token, uint256 amount);
event SupplyUpdated(address token, uint256 supply);
constructor(ISigsVerifier _sigsVerifier) {
sigsVerifier = _sigsVerifier;
}
/**
* @notice Mint tokens triggered by deposit at a remote chain's OriginalTokenVault.
* @param _request The serialized Mint protobuf.
* @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by
* +2/3 of the sigsVerifier's current signing power to be delivered.
* @param _signers The sorted list of signers.
* @param _powers The signing powers of the signers.
*/
function mint(
bytes calldata _request,
bytes[] calldata _sigs,
address[] calldata _signers,
uint256[] calldata _powers
) external whenNotPaused returns (bytes32) {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Mint"));
sigsVerifier.verifySigs(abi.encodePacked(domain, _request), _sigs, _signers, _powers);
PbPegged.Mint memory request = PbPegged.decMint(_request);
bytes32 mintId = keccak256(
// len = 20 + 20 + 32 + 20 + 8 + 32 + 20 = 152
abi.encodePacked(
request.account,
request.token,
request.amount,
request.depositor,
request.refChainId,
request.refId,
address(this)
)
);
require(records[mintId] == false, "record exists");
records[mintId] = true;
_updateVolume(request.token, request.amount);
uint256 delayThreshold = delayThresholds[request.token];
if (delayThreshold > 0 && request.amount > delayThreshold) {
_addDelayedTransfer(mintId, request.account, request.token, request.amount);
} else {
IPeggedToken(request.token).mint(request.account, request.amount);
}
supplies[request.token] += request.amount;
emit Mint(
mintId,
request.token,
request.account,
request.amount,
request.refChainId,
request.refId,
request.depositor
);
return mintId;
}
/**
* @notice Burn pegged tokens to trigger a cross-chain withdrawal of the original tokens at a remote chain's
* OriginalTokenVault, or mint at another remote chain
* NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens.
* @param _token The pegged token address.
* @param _amount The amount to burn.
* @param _toChainId If zero, withdraw from original vault; otherwise, the remote chain to mint tokens.
* @param _toAccount The account to receive tokens on the remote chain
* @param _nonce A number to guarantee unique depositId. Can be timestamp in practice.
*/
function burn(
address _token,
uint256 _amount,
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) external whenNotPaused returns (bytes32) {
bytes32 burnId = _burn(_token, _amount, _toChainId, _toAccount, _nonce);
IPeggedToken(_token).burn(msg.sender, _amount);
return burnId;
}
// same with `burn` above, use openzeppelin ERC20Burnable interface
function burnFrom(
address _token,
uint256 _amount,
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) external whenNotPaused returns (bytes32) {
bytes32 burnId = _burn(_token, _amount, _toChainId, _toAccount, _nonce);
IPeggedTokenBurnFrom(_token).burnFrom(msg.sender, _amount);
return burnId;
}
function _burn(
address _token,
uint256 _amount,
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) internal returns (bytes32) {
require(_amount > minBurn[_token], "amount too small");
require(maxBurn[_token] == 0 || _amount <= maxBurn[_token], "amount too large");
supplies[_token] -= _amount;
bytes32 burnId = keccak256(
// len = 20 + 20 + 32 + 8 + 20 + 8 + 8 + 20 = 136
abi.encodePacked(
msg.sender,
_token,
_amount,
_toChainId,
_toAccount,
_nonce,
uint64(block.chainid),
address(this)
)
);
require(records[burnId] == false, "record exists");
records[burnId] = true;
emit Burn(burnId, _token, msg.sender, _amount, _toChainId, _toAccount, _nonce);
return burnId;
}
function executeDelayedTransfer(bytes32 id) external whenNotPaused {
delayedTransfer memory transfer = _executeDelayedTransfer(id);
IPeggedToken(transfer.token).mint(transfer.receiver, transfer.amount);
}
function setMinBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
minBurn[_tokens[i]] = _amounts[i];
emit MinBurnUpdated(_tokens[i], _amounts[i]);
}
}
function setMaxBurn(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor {
require(_tokens.length == _amounts.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
maxBurn[_tokens[i]] = _amounts[i];
emit MaxBurnUpdated(_tokens[i], _amounts[i]);
}
}
function setSupply(address _token, uint256 _supply) external onlyOwner {
supplies[_token] = _supply;
emit SupplyUpdated(_token, _supply);
}
function increaseSupply(address _token, uint256 _delta) external onlyOwner {
supplies[_token] += _delta;
emit SupplyUpdated(_token, supplies[_token]);
}
function decreaseSupply(address _token, uint256 _delta) external onlyOwner {
supplies[_token] -= _delta;
emit SupplyUpdated(_token, supplies[_token]);
}
}
================================================
FILE: contracts/pegged-bridge/README.md
================================================
# Pegged Token Bridge
Goal: Token T exists on chain A but not on chain B, and we would like to support a 1:1 pegged token T' on chain B.
Approach: Deploy a PeggedToken ([example](./tokens/MultiBridgeToken.sol)) on chain B with zero initial supply, and config SGN (through gov) to mark it as 1:1 pegged to the chain A’s original token. Anyone can lock original token T on chain A’s OriginalTokenVault contract to trigger mint of pegged token T’ on chain B through the PeggedTokenBridge contract accordingly.
## Basic workflows
### Deposit original token on chain A and mint pegged token on chain B
1. User calls [deposit](./OriginalTokenVault.sol#L72) on chain A to lock original tokens in chain A’s vault contract.
2. SGN generates the [Mint proto msg](../libraries/proto/pegged.proto#L14) cosigned by validators, and call [mint](./PeggedTokenBridge.sol#L55) function on chain B.
### Burn pegged token on chain B and withdraw original token on chain A
1. User calls [burn](./PeggedTokenBridge.sol#L104) on chain B to burn the pegged token.
2. SGN generates the [Withdraw proto msg](../libraries/proto/pegged.proto#L34) cosigned by validators, and call [withdraw](./OriginalTokenVault.sol#L131) function on chain A.
### Burn pegged token on chain B (PeggedTokenBridgeV2) and mint pegged token on chain C
1. User calls [burn](./PeggedTokenBridgeV2.sol#L116) on chain B to burn the pegged token, specifying chain C's chainId as `toChainId`.
2. SGN generates the [Mint proto msg](../libraries/proto/pegged.proto#L14) cosigned by validators, and call [mint](./PeggedTokenBridge.sol#L55) function on chain C.
## Safeguard monitoring
Anyone can verify the correctness of the pegged bridge behavior by tracking the contract events ([Deposit](./OriginalTokenVault.sol#L31), [Withdrawn](./OriginalTokenVault.sol#L39), [Mint](./PeggedTokenBridge.sol#L24), [Burn](./PeggedTokenBridge.sol#L39)), and verifying the `refChainId` and `refId` fields of `Mint` and `Withdrawn` events according to the code comments ([example](./OriginalTokenVault.sol#L44-L53)).
For example, if we catch a `Withdrawn` event on chain A with `refChainId` of chain B and a `refId`, then we should be able to find a `Burn` event on chain B with `burnId` equals to `refId`, and then compare values of other fields of these two events.
================================================
FILE: contracts/pegged-bridge/customized/PeggedNativeTokenBridge.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../PeggedTokenBridgeV2.sol";
interface INativeVault {
function burn() external payable;
}
contract PeggedNativeTokenBridge is PeggedTokenBridgeV2 {
// native vault address is treated as the pegged natvie token address
address public nativeVault;
constructor(ISigsVerifier _sigsVerifier) PeggedTokenBridgeV2(_sigsVerifier) {}
function burnNative(
uint64 _toChainId,
address _toAccount,
uint64 _nonce
) external payable whenNotPaused returns (bytes32) {
require(msg.value > 0, "zero msg value");
bytes32 burnId = _burn(nativeVault, msg.value, _toChainId, _toAccount, _nonce);
INativeVault(nativeVault).burn{value: msg.value}();
return burnId;
}
function setNativeVault(address _natvieVault) external onlyOwner {
nativeVault = _natvieVault;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/ERC20Permit/MintSwapCanonicalTokenPermit.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "../MintSwapCanonicalToken.sol";
/**
* @title MintSwapCanonicalToke with ERC20Permit
*/
contract MintSwapCanonicalTokenPermit is ERC20Permit, MintSwapCanonicalToken {
uint8 private immutable _decimals;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MintSwapCanonicalToken(name_, symbol_, decimals_) ERC20Permit(name_) {
_decimals = decimals_;
}
function decimals() public view override(ERC20, MultiBridgeToken) returns (uint8) {
return _decimals;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/ERC20Permit/MultiBridgeTokenPermit.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "../MultiBridgeToken.sol";
/**
* @title Example Multi-Bridge Pegged ERC20Permit token
*/
contract MultiBridgeTokenPermit is ERC20Permit, MultiBridgeToken {
uint8 private immutable _decimals;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MultiBridgeToken(name_, symbol_, decimals_) ERC20Permit(name_) {
_decimals = decimals_;
}
function decimals() public view override(ERC20, MultiBridgeToken) returns (uint8) {
return _decimals;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/ERC20Permit/SingleBridgeTokenPermit.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "../SingleBridgeToken.sol";
/**
* @title Example Pegged ERC20Permit token
*/
contract SingleBridgeTokenPermit is ERC20Permit, SingleBridgeToken {
uint8 private immutable _decimals;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address bridge_
) SingleBridgeToken(name_, symbol_, decimals_, bridge_) ERC20Permit(name_) {
_decimals = decimals_;
}
function decimals() public view override(ERC20, SingleBridgeToken) returns (uint8) {
return _decimals;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/IntermediaryBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface IERC20MintableBurnable is IERC20 {
function mint(address receiver, uint256 amount) external;
function burn(uint256 amount) external;
}
/**
* @title Per bridge intermediary token that delegates to a canonical token.
* Useful for canonical tokens that don't support the burn / burnFrom function signature required by
* PeggedTokenBridge.
*/
contract IntermediaryBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
address public bridge;
address public immutable canonical; // canonical token that support swap
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address canonical_
) ERC20(name_, symbol_) {
bridge = bridge_;
canonical = canonical_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount); // totalSupply == bridge liquidity
IERC20MintableBurnable(canonical).mint(_to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
_burn(address(this), _amount);
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
IERC20MintableBurnable(canonical).burn(_amount);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/IntermediaryOriginalToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title Intermediary token that automatically transfers the canonical token when interacting with approved bridges.
*/
contract IntermediaryOriginalToken is ERC20, Ownable {
using SafeERC20 for IERC20;
mapping(address => bool) public bridges;
address public immutable canonical; // canonical token
event BridgeUpdated(address bridge, bool enable);
modifier onlyBridge() {
require(bridges[msg.sender], "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address[] memory bridges_,
address canonical_
) ERC20(name_, symbol_) {
for (uint256 i = 0; i < bridges_.length; i++) {
bridges[bridges_[i]] = true;
}
canonical = canonical_;
}
function transfer(address _to, uint256 _amount) public virtual override onlyBridge returns (bool) {
_burn(msg.sender, _amount);
IERC20(canonical).safeTransfer(_to, _amount);
return true;
}
function transferFrom(
address _from,
address _to,
uint256 _amount
) public virtual override onlyBridge returns (bool) {
_mint(_to, _amount);
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
return true;
}
function updateBridge(address _bridge, bool _enable) external onlyOwner {
bridges[_bridge] = _enable;
emit BridgeUpdated(_bridge, _enable);
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/MintSwapCanonicalToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./MultiBridgeToken.sol";
/**
* @title Canonical token that supports multi-bridge minter and multi-token swap
*/
contract MintSwapCanonicalToken is MultiBridgeToken {
using SafeERC20 for IERC20;
// bridge token address -> minted amount and cap for each bridge
mapping(address => Supply) public swapSupplies;
event TokenSwapCapUpdated(address token, uint256 cap);
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MultiBridgeToken(name_, symbol_, decimals_) {}
/**
* @notice msg.sender has bridge token and wants to get canonical token.
* @param _bridgeToken The intermediary token address for a particular bridge.
* @param _amount The amount.
*/
function swapBridgeForCanonical(address _bridgeToken, uint256 _amount) external returns (uint256) {
Supply storage supply = swapSupplies[_bridgeToken];
require(supply.cap > 0, "invalid bridge token");
require(supply.total + _amount <= supply.cap, "exceed swap cap");
supply.total += _amount;
_mint(msg.sender, _amount);
// move bridge token from msg.sender to canonical token _amount
IERC20(_bridgeToken).safeTransferFrom(msg.sender, address(this), _amount);
return _amount;
}
/**
* @notice msg.sender has canonical token and wants to get bridge token (eg. for cross chain burn).
* @param _bridgeToken The intermediary token address for a particular bridge.
* @param _amount The amount.
*/
function swapCanonicalForBridge(address _bridgeToken, uint256 _amount) external returns (uint256) {
Supply storage supply = swapSupplies[_bridgeToken];
require(supply.cap > 0, "invalid bridge token");
supply.total -= _amount;
_burn(msg.sender, _amount);
IERC20(_bridgeToken).safeTransfer(msg.sender, _amount);
return _amount;
}
/**
* @dev Update existing bridge token swap cap or add a new bridge token with swap cap.
* Setting cap to 0 will disable the bridge token.
* @param _bridgeToken The intermediary token address for a particular bridge.
* @param _swapCap The new swap cap.
*/
function setBridgeTokenSwapCap(address _bridgeToken, uint256 _swapCap) external onlyOwner {
swapSupplies[_bridgeToken].cap = _swapCap;
emit TokenSwapCapUpdated(_bridgeToken, _swapCap);
}
}
================================================
FILE: contracts/pegged-bridge/tokens/MintSwapCanonicalTokenUpgradable.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "./MintSwapCanonicalToken.sol";
/**
* @title Upgradable canonical token that supports multi-bridge minter and multi-token swap
*/
// First deploy this contract, constructor will set name, symbol and owner in contract state, but these are NOT used.
// decimal isn't saved in state because it's immutable in MultiBridgeToken and will be set in the code binary.
// Then deploy proxy contract with this contract as impl, proxy constructor will delegatecall this.init which sets name, symbol and owner in proxy contract state.
// why we need to shadow name and symbol: ERC20 only allows set them in constructor which isn't available after deploy so proxy state can't be updated.
contract MintSwapCanonicalTokenUpgradable is MintSwapCanonicalToken {
string private _name;
string private _symbol;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MintSwapCanonicalToken(name_, symbol_, decimals_) {}
// only to be called by Proxy via delegatecall and will modify Proxy state
// this func has no access control because initOwner only allows delegateCall
function init(string memory name_, string memory symbol_) external {
initOwner(); // this will fail if Ownable._owner is already set
_name = name_;
_symbol = symbol_;
}
// override name, symbol and owner getters
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/MultiBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../../safeguard/Ownable.sol";
/**
* @title Example Multi-Bridge Pegged ERC20 token
*/
contract MultiBridgeToken is ERC20, Ownable {
struct Supply {
uint256 cap;
uint256 total;
}
mapping(address => Supply) public bridges; // bridge address -> supply
uint8 private immutable _decimals;
event BridgeSupplyCapUpdated(address bridge, uint256 supplyCap);
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) ERC20(name_, symbol_) {
_decimals = decimals_;
}
/**
* @notice Mints tokens to an address. Increases total amount minted by the calling bridge.
* @param _to The address to mint tokens to.
* @param _amount The amount to mint.
*/
function mint(address _to, uint256 _amount) external returns (bool) {
Supply storage b = bridges[msg.sender];
require(b.cap > 0, "invalid caller");
b.total += _amount;
require(b.total <= b.cap, "exceeds bridge supply cap");
_mint(_to, _amount);
return true;
}
/**
* @notice Burns tokens for msg.sender.
* @param _amount The amount to burn.
*/
function burn(uint256 _amount) external returns (bool) {
_burn(msg.sender, _amount);
return true;
}
/**
* @notice Burns tokens from an address. Decreases total amount minted if called by a bridge.
* Alternative to {burnFrom} for compatibility with some bridge implementations.
* See {_burnFrom}.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function burn(address _from, uint256 _amount) external returns (bool) {
return _burnFrom(_from, _amount);
}
/**
* @notice Burns tokens from an address. Decreases total amount minted if called by a bridge.
* See {_burnFrom}.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function burnFrom(address _from, uint256 _amount) external returns (bool) {
return _burnFrom(_from, _amount);
}
/**
* @dev Burns tokens from an address, deducting from the caller's allowance.
* Decreases total amount minted if called by a bridge.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function _burnFrom(address _from, uint256 _amount) internal returns (bool) {
Supply storage b = bridges[msg.sender];
if (b.cap > 0 || b.total > 0) {
// set cap to 1 would effectively disable a deprecated bridge's ability to burn
require(b.total >= _amount, "exceeds bridge minted amount");
unchecked {
b.total -= _amount;
}
}
_spendAllowance(_from, msg.sender, _amount);
_burn(_from, _amount);
return true;
}
/**
* @notice Returns the decimals of the token.
*/
function decimals() public view virtual override returns (uint8) {
return _decimals;
}
/**
* @notice Updates the supply cap for a bridge.
* @param _bridge The bridge address.
* @param _cap The new supply cap.
*/
function updateBridgeSupplyCap(address _bridge, uint256 _cap) external onlyOwner {
// cap == 0 means revoking bridge role
bridges[_bridge].cap = _cap;
emit BridgeSupplyCapUpdated(_bridge, _cap);
}
/**
* @notice Returns the owner address. Required by BEP20.
*/
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/SingleBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title Example Pegged ERC20 token
*/
contract SingleBridgeToken is ERC20, Ownable {
address public bridge;
uint8 private immutable _decimals;
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address bridge_
) ERC20(name_, symbol_) {
_decimals = decimals_;
bridge = bridge_;
}
/**
* @notice Mints tokens to an address.
* @param _to The address to mint tokens to.
* @param _amount The amount to mint.
*/
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(_to, _amount);
return true;
}
/**
* @notice Burns tokens for msg.sender.
* @param _amount The amount to burn.
*/
function burn(uint256 _amount) external returns (bool) {
_burn(msg.sender, _amount);
return true;
}
/**
* @notice Burns tokens from an address.
* Alternative to {burnFrom} for compatibility with some bridge implementations.
* See {_burnFrom}.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function burn(address _from, uint256 _amount) external returns (bool) {
return _burnFrom(_from, _amount);
}
/**
* @notice Burns tokens from an address.
* See {_burnFrom}.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function burnFrom(address _from, uint256 _amount) external returns (bool) {
return _burnFrom(_from, _amount);
}
/**
* @dev Burns tokens from an address, deducting from the caller's allowance.
* @param _from The address to burn tokens from.
* @param _amount The amount to burn.
*/
function _burnFrom(address _from, uint256 _amount) internal returns (bool) {
_spendAllowance(_from, msg.sender, _amount);
_burn(_from, _amount);
return true;
}
/**
* @notice Returns the decimals of the token.
*/
function decimals() public view virtual override returns (uint8) {
return _decimals;
}
/**
* @notice Updates the bridge address.
* @param _bridge The bridge address.
*/
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
/**
* @notice Returns the owner address. Required by BEP20.
*/
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/SwapBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface ISwapCanoToken {
function swapBridgeForCanonical(address, uint256) external returns (uint256);
function swapCanonicalForBridge(address, uint256) external returns (uint256);
}
/**
* @title Per bridge intermediary token that supports swapping with a canonical token.
*/
contract SwapBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
address public bridge;
address public immutable canonical; // canonical token that support swap
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address canonical_
) ERC20(name_, symbol_) {
bridge = bridge_;
canonical = canonical_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount); // add amount to myself so swapBridgeForCanonical can transfer amount
uint256 got = ISwapCanoToken(canonical).swapBridgeForCanonical(address(this), _amount);
// now this has canonical token, next step is to transfer to user
IERC20(canonical).safeTransfer(_to, got);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
uint256 got = ISwapCanoToken(canonical).swapCanonicalForBridge(address(this), _amount);
_burn(address(this), got);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
// approve canonical token so swapBridgeForCanonical can work. or we approve before call it in mint w/ added gas
function approveCanonical() external onlyOwner {
_approve(address(this), canonical, type(uint256).max);
}
function revokeCanonical() external onlyOwner {
_approve(address(this), canonical, 0);
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/WrappedBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// Use pegged model to support no-slippage liquidity pool
contract WrappedBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
// The PeggedTokenBridge
address public bridge;
// The canonical
address public immutable canonical;
mapping(address => uint256) public liquidity;
event BridgeUpdated(address bridge);
event LiquidityAdded(address provider, uint256 amount);
event LiquidityRemoved(address provider, uint256 amount);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address canonical_
) ERC20(name_, symbol_) {
bridge = bridge_;
canonical = canonical_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount);
IERC20(canonical).safeTransfer(_to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
_burn(address(this), _amount);
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
return true;
}
function addLiquidity(uint256 _amount) external {
liquidity[msg.sender] += _amount;
IERC20(canonical).safeTransferFrom(msg.sender, address(this), _amount);
emit LiquidityAdded(msg.sender, _amount);
}
function removeLiquidity(uint256 _amount) external {
liquidity[msg.sender] -= _amount;
IERC20(canonical).safeTransfer(msg.sender, _amount);
emit LiquidityRemoved(msg.sender, _amount);
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/CircleBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../../../libraries/BridgeTransferLib.sol";
// Interface for the canonical Circle token
interface IFiatToken is IERC20 {
function mint(address to, uint256 amount) external;
// Burns OWN tokens
function burn(uint256 amount) external;
}
/**
* @title Intermediary token that delegates to a canonical Circle token. With the functionality to
* migrate minting / burning permissions to Circle in the future.
*/
contract CircleBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
address public bridge;
address public immutable canonical; // canonical Circle token
bool public migrated;
uint64 public immutable origChainId;
address public origChainWithdrawAddress;
event BridgeUpdated(address bridge);
event OrigChainWithdrawAddressUpdated(address origChainWithdrawAddress);
event Migrated(bytes32 transferId, address origChainWithdrawAddress, uint256 totalSupply);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address canonical_,
uint64 origChainId_
) ERC20(name_, symbol_) {
bridge = bridge_;
canonical = canonical_;
origChainId = origChainId_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
require(!migrated, "already migrated");
_mint(address(this), _amount); // totalSupply == bridge liquidity
IFiatToken(canonical).mint(_to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
_burn(address(this), _amount);
if (!migrated) {
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
IFiatToken(canonical).burn(_amount);
}
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
function setOrigChainWithdrawAddress(address _origChainWithdrawAddress) external onlyOwner {
origChainWithdrawAddress = _origChainWithdrawAddress;
emit OrigChainWithdrawAddressUpdated(origChainWithdrawAddress);
}
function migrate() external onlyOwner {
require(!migrated, "already migrated");
migrated = true;
uint256 supply = totalSupply();
bytes32 transferId = BridgeTransferLib.sendTransfer(
origChainWithdrawAddress,
address(this),
supply,
origChainId,
uint64(block.timestamp),
0, // _maxSlippage
BridgeTransferLib.BridgeSendType.PegV2Burn,
bridge
);
emit Migrated(transferId, origChainWithdrawAddress, supply);
}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/FraxBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface IFraxCanoToken {
function exchangeOldForCanonical(address, uint256) external returns (uint256);
function exchangeCanonicalForOld(address, uint256) external returns (uint256);
}
/**
* @title Intermediary bridge token that supports swapping with the canonical Frax token.
*/
contract FraxBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
// The PeggedTokenBridge
address public bridge;
// The canonical Frax token that supports swapping
address public immutable canonical;
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address canonical_
) ERC20(name_, symbol_) {
bridge = bridge_;
canonical = canonical_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount); // add amount to myself so exchangeOldForCanonical can transfer amount
_approve(address(this), canonical, _amount);
uint256 got = IFraxCanoToken(canonical).exchangeOldForCanonical(address(this), _amount);
// now this has canonical token, next step is to transfer to user
IERC20(canonical).safeTransfer(_to, got);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
uint256 got = IFraxCanoToken(canonical).exchangeCanonicalForOld(address(this), _amount);
_burn(address(this), got);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/MaiBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface IMaiBridgeHub {
// send bridge token, get asset
function swapIn(address, uint256) external;
// send asset, get bridge token back
function swapOut(address, uint256) external;
// asset address
function asset() external view returns (address);
}
/**
* @title Intermediary bridge token that supports swapping with the Mai hub.
* NOTE: Mai hub is NOT the canonical token itself. The asset is set in the hub constructor.
*/
contract MaiBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
// The PeggedTokenBridge
address public bridge;
// Mai hub for swapping
address public immutable maihub;
// The canonical Mai token
address public immutable asset;
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address maihub_
) ERC20(name_, symbol_) {
bridge = bridge_;
maihub = maihub_;
asset = IMaiBridgeHub(maihub_).asset();
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount); // add amount to myself so swapIn can transfer amount to hub
_approve(address(this), maihub, _amount);
IMaiBridgeHub(maihub).swapIn(address(this), _amount);
// now this has canonical token, next step is to transfer to user
IERC20(asset).safeTransfer(_to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
IERC20(asset).safeTransferFrom(_from, address(this), _amount);
IERC20(asset).safeIncreaseAllowance(address(maihub), _amount);
IMaiBridgeHub(maihub).swapOut(address(this), _amount);
_burn(address(this), _amount);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
function decimals() public view virtual override returns (uint8) {
return ERC20(asset).decimals();
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/OntologyBridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface IOntologyBridgeTokenWrapper {
function swapBridgeForCanonical(
address bridgeToken,
address _to,
uint256 _amount
) external returns (uint256);
function swapCanonicalForBridge(
address bridgeToken,
address _to,
uint256 _amount
) external payable returns (uint256);
}
/**
* @title Intermediary bridge token that supports swapping with the Ontology bridge token wrapper.
* NOTE: The bridge wrapper is NOT the canonical token itself.
*/
contract OntologyBridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
// The PeggedTokenBridge
address public bridge;
// Bridge token wrapper for swapping
address public immutable wrapper;
// The canonical token
address public immutable canonical;
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address bridge_,
address wrapper_,
address canonical_
) ERC20(name_, symbol_) {
bridge = bridge_;
wrapper = wrapper_;
canonical = canonical_;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount);
_approve(address(this), wrapper, _amount);
// NOTE: swapBridgeForCanonical automatically transfers canonical token to _to.
IOntologyBridgeTokenWrapper(wrapper).swapBridgeForCanonical(address(this), _to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
IERC20(canonical).safeTransferFrom(_from, address(this), _amount);
IERC20(canonical).safeIncreaseAllowance(address(wrapper), _amount);
// NOTE: swapCanonicalForBridge automatically transfers bridge token to _from.
uint256 got = IOntologyBridgeTokenWrapper(wrapper).swapCanonicalForBridge(address(this), _from, _amount);
_burn(_from, got);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonical).decimals();
}
// to make compatible with BEP20
function getOwner() external view returns (address) {
return owner();
}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/xc20/XC20BridgeHub.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/IXC20BridgeHub.sol";
/**
* @title A hub for managing swapping between canonical XC20 tokens and intermediary bridge tokens.
*/
contract XC20BridgeHub is Ownable, IXC20BridgeHub, Pausable {
using SafeERC20 for IERC20;
struct TokenPair {
address bridgeToken;
address canonicalToken;
bool paused;
uint256 limit; // Max amount of bridge token allowed in the hub.
}
address[] public bridgeTokens;
// bridge token address => TokenPair
mapping(address => TokenPair) public tokenPairMap;
event TokenPairAdded(address indexed bridgeToken, address indexed canonicalToken, uint256 limit);
event TokenPairRemoved(address indexed bridgeToken, address indexed canonicalToken);
event TokenPairPaused(address indexed bridgeToken, address indexed canonicalToken);
event TokenPairUnpaused(address indexed bridgeToken, address indexed canonicalToken);
event TokenPairLimitSet(address indexed bridgeToken, address indexed canonicalToken, uint256 limit);
event BridgeSwappedForCanonical(
address indexed bridgeToken,
address indexed canonicalToken,
uint256 bridgeTokenAmount,
uint256 refund,
uint256 canonicalTokenAmount
);
event CanonicalSwappedForBridge(
address indexed bridgeToken,
address indexed canonicalToken,
uint256 canonicalTokenAmount,
uint256 refund,
uint256 bridgeTokenAmount
);
/**
* @dev Pauses a token pair.
* @param _bridgeToken The bridge token of the pair.
*/
function pauseTokenPair(address _bridgeToken) external onlyOwner {
TokenPair storage pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
require(!pair.paused, "XC20BridgeHub: token pair already paused");
pair.paused = true;
emit TokenPairPaused(_bridgeToken, pair.canonicalToken);
}
/**
* @dev Unpauses a token pair.
* @param _bridgeToken The bridge token of the pair.
*/
function unpauseTokenPair(address _bridgeToken) external onlyOwner {
TokenPair storage pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
require(pair.paused, "XC20BridgeHub: token pair already unpaused");
pair.paused = false;
emit TokenPairUnpaused(_bridgeToken, pair.canonicalToken);
}
/**
* @dev Sets a token pair limit.
* @param _bridgeToken The bridge token of the pair.
* @param _limit The max amount of bridge token allowed in the hub.
*/
function setTokenPairLimit(address _bridgeToken, uint256 _limit) external onlyOwner {
TokenPair storage pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
pair.limit = _limit;
emit TokenPairLimitSet(_bridgeToken, pair.canonicalToken, _limit);
}
/**
* @dev Adds a token pair.
* @param _bridgeToken The bridge token of the pair.
* @param _canonicalToken The canonical token of the pair.
* @param _limit The max amount of bridge token allowed in the hub.
*/
function addTokenPair(
address _bridgeToken,
address _canonicalToken,
uint256 _limit
) external onlyOwner {
require(_bridgeToken != address(0), "XC20BridgeHub: bridge token is zero address");
require(tokenPairMap[_bridgeToken].bridgeToken == address(0), "XC20BridgeHub: bridge token exists");
require(
IERC20Metadata(_bridgeToken).decimals() == IERC20Metadata(_canonicalToken).decimals(),
"XC20BridgeHub: decimals mismatch"
);
TokenPair memory pair = TokenPair(address(_bridgeToken), address(_canonicalToken), false, _limit);
bridgeTokens.push(_bridgeToken);
tokenPairMap[_bridgeToken] = pair;
emit TokenPairAdded(_bridgeToken, _canonicalToken, _limit);
}
/**
* @dev Removes a token pair.
* @param _bridgeToken The bridge token of the pair.
*/
function removeTokenPair(address _bridgeToken) external onlyOwner {
TokenPair memory pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
delete tokenPairMap[_bridgeToken];
uint256 index = bridgeTokens.length;
for (uint256 i = 0; i < bridgeTokens.length; i++) {
if (bridgeTokens[i] == _bridgeToken) {
index = i;
break;
}
}
if (index < bridgeTokens.length) {
delete bridgeTokens[index];
}
emit TokenPairRemoved(_bridgeToken, pair.canonicalToken);
}
/**
* @dev Returns all token pairs.
*/
function getAllTokenPairs() external view returns (TokenPair[] memory) {
TokenPair[] memory pairs = new TokenPair[](bridgeTokens.length);
for (uint256 i = 0; i < pairs.length; i++) {
pairs[i] = tokenPairMap[bridgeTokens[i]];
}
return pairs;
}
/**
* @dev Swaps intermediary bridge token for canonical XC-20 token.
* @param _bridgeToken The intermediary bridge token
* @param _amount The amount to swap
* @return The canonical token amount
*/
function swapBridgeForCanonical(address _bridgeToken, uint256 _amount)
external
override
whenNotPaused
returns (uint256)
{
TokenPair memory pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
require(!pair.paused, "XC20BridgeHub: token pair paused");
IERC20 bridgeErc20 = IERC20(_bridgeToken);
require(
pair.limit > 0 && (bridgeErc20.balanceOf(address(this))) + _amount <= pair.limit,
"XC20BridgeHub: exceeds bridge limit"
);
address canonicalToken = pair.canonicalToken;
uint256 delta = transferIn(_bridgeToken, _amount);
(uint256 canonicalTokenAmount, uint256 refund) = calcTransferAmountWithDecimals(
delta,
IERC20Metadata(_bridgeToken).decimals(),
IERC20Metadata(canonicalToken).decimals()
);
if (refund > 0) {
IERC20(_bridgeToken).safeTransfer(msg.sender, refund);
}
if (canonicalTokenAmount > 0) {
IERC20(pair.canonicalToken).safeTransfer(msg.sender, _amount);
}
emit BridgeSwappedForCanonical(_bridgeToken, canonicalToken, _amount, refund, canonicalTokenAmount);
return canonicalTokenAmount;
}
/**
* @dev Swaps canonical XC-20 token for intermediary bridge token.
* @param _bridgeToken The intermediary bridge token
* @param _amount The amount to swap
* @return The bridge token amount
*/
function swapCanonicalForBridge(address _bridgeToken, uint256 _amount)
external
override
whenNotPaused
returns (uint256)
{
TokenPair memory pair = tokenPairMap[_bridgeToken];
require(pair.bridgeToken != address(0), "XC20BridgeHub: non-existent bridge token");
require(!pair.paused, "XC20BridgeHub: token pair paused");
address canonicalToken = pair.canonicalToken;
uint256 delta = transferIn(canonicalToken, _amount);
(uint256 bridgeTokenAmount, uint256 refund) = calcTransferAmountWithDecimals(
delta,
IERC20Metadata(canonicalToken).decimals(),
IERC20Metadata(_bridgeToken).decimals()
);
if (refund > 0) {
IERC20(canonicalToken).safeTransfer(msg.sender, refund);
}
if (bridgeTokenAmount > 0) {
IERC20(_bridgeToken).safeTransfer(msg.sender, _amount);
}
emit CanonicalSwappedForBridge(_bridgeToken, canonicalToken, _amount, refund, bridgeTokenAmount);
return bridgeTokenAmount;
}
/**
* @dev Sets the paused status of the hub.
* @param _paused Whether the hub should be paused
*/
function setPaused(bool _paused) external onlyOwner {
if (_paused) {
_pause();
} else {
_unpause();
}
}
/**
* @dev Transfers the amount of tokens into the hub.
* @param _token The token address
* @param _amount The transfer amount
* @return The balance change
*/
function transferIn(address _token, uint256 _amount) internal returns (uint256) {
uint256 balanceBefore = IERC20(_token).balanceOf(address(this));
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
uint256 balanceAfter = IERC20(_token).balanceOf(address(this));
return balanceAfter - balanceBefore;
}
/**
* @dev Calculates the transfer amount and if applicable, refund amount, taking into account the
* difference between token decimals.
* @param _amount The original amount
* @param _aDecimals The decimals of token A
* @param _bDecimals The decimals of token B
*/
function calcTransferAmountWithDecimals(
uint256 _amount,
uint256 _aDecimals,
uint256 _bDecimals
) internal pure returns (uint256 newAmount, uint256 refund) {
if (_aDecimals > _bDecimals) {
newAmount = _amount / (10**(_aDecimals - _bDecimals));
refund = _amount - newAmount * (10**(_aDecimals - _bDecimals));
} else if (_aDecimals < _bDecimals) {
newAmount = _amount * (10**(_bDecimals - _aDecimals));
} else {
newAmount = _amount;
}
}
// This account has to hold some amount of native currency in order to be eligible
// to receive canonical x20 assets per Astar rule
receive() external payable {}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/xc20/XC20BridgeToken.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./interfaces/IXC20BridgeHub.sol";
/**
* @title Intermediary bridge token that supports swapping with the XC-20 bridge hub.
* NOTE: XC-20 bridge hub is NOT the canonical token itself.
*/
contract XC20BridgeToken is ERC20, Ownable {
using SafeERC20 for IERC20;
// The pegged token bridge
address public bridge;
// XC20 bridge hub for swapping
address public immutable bridgeHub;
// The canonical token
address public immutable canonicalToken;
event BridgeUpdated(address bridge);
modifier onlyBridge() {
require(msg.sender == bridge, "XC20BridgeToken: caller is not bridge");
_;
}
constructor(
string memory name_,
string memory symbol_,
address _bridge,
address _bridgeHub,
address _canonicalToken
) ERC20(name_, symbol_) {
bridge = _bridge;
bridgeHub = _bridgeHub;
canonicalToken = _canonicalToken;
}
function mint(address _to, uint256 _amount) external onlyBridge returns (bool) {
_mint(address(this), _amount); // Mint to this contract to be transferred to the hub
_approve(address(this), bridgeHub, _amount);
IXC20BridgeHub(bridgeHub).swapBridgeForCanonical(address(this), _amount);
// Now this has canonical token, next step is to transfer to user.
IERC20(canonicalToken).safeTransfer(_to, _amount);
return true;
}
function burn(address _from, uint256 _amount) external onlyBridge returns (bool) {
IERC20(canonicalToken).safeTransferFrom(_from, address(this), _amount);
IERC20(canonicalToken).safeIncreaseAllowance(address(bridgeHub), _amount);
IXC20BridgeHub(bridgeHub).swapCanonicalForBridge(address(this), _amount);
_burn(address(this), _amount);
return true;
}
function updateBridge(address _bridge) external onlyOwner {
bridge = _bridge;
emit BridgeUpdated(bridge);
}
function decimals() public view virtual override returns (uint8) {
return ERC20(canonicalToken).decimals();
}
// For compatibility with BEP20
function getOwner() external view returns (address) {
return owner();
}
// This account has to hold some amount of native currency in order to be eligible
// to receive canonical x20 assets per Astar rule
receive() external payable {}
}
================================================
FILE: contracts/pegged-bridge/tokens/customized/xc20/interfaces/IXC20BridgeHub.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;
interface IXC20BridgeHub {
/**
* @dev Swaps intermediary bridge token for canonical XC-20 token.
* @param bridgeToken The intermediary bridge token
* @param amount The amount to swap
* @return The canonical token amount
*/
function swapBridgeForCanonical(address bridgeToken, uint256 amount) external returns (uint256);
/**
* @dev Swaps canonical XC-20 token for intermediary bridge token.
* @param bridgeToken The intermediary bridge token
* @param amount The amount to swap
* @return The bridge token amount
*/
function swapCanonicalForBridge(address bridgeToken, uint256 amount) external returns (uint256);
}
================================================
FILE: contracts/pegged-bridge/tokens/freezable/Freezable.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
abstract contract Freezable {
event Frozen(address account);
event Unfrozen(address account);
mapping(address => bool) internal freezes;
function isFrozen(address _account) public view virtual returns (bool) {
return freezes[_account];
}
modifier whenAccountNotFrozen(address _account) {
require(!isFrozen(_account), "Freezable: frozen");
_;
}
modifier whenAccountFrozen(address _account) {
require(isFrozen(_account), "Freezable: not frozen");
_;
}
}
================================================
FILE: contracts/pegged-bridge/tokens/freezable/MintSwapCanonicalTokenFreezable.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Freezable.sol";
import "../MintSwapCanonicalToken.sol";
/**
* @title Canonical token that supports multi-bridge minter and multi-token swap. Support freezable erc20 transfer
*/
contract MintSwapCanonicalTokenFreezable is MintSwapCanonicalToken, Freezable {
string private _name;
string private _symbol;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MintSwapCanonicalToken(name_, symbol_, decimals_) {}
// freezable related
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
require(!isFrozen(from), "ERC20Freezable: from account is frozen");
require(!isFrozen(to), "ERC20Freezable: to account is frozen");
}
function freeze(address _account) public onlyOwner {
freezes[_account] = true;
emit Frozen(_account);
}
function unfreeze(address _account) public onlyOwner {
freezes[_account] = false;
emit Unfrozen(_account);
}
}
================================================
FILE: contracts/pegged-bridge/tokens/freezable/MintSwapCanonicalTokenUpgradableFreezable.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Freezable.sol";
import "../MintSwapCanonicalTokenUpgradable.sol";
/**
* @title Upgradable canonical token that supports multi-bridge minter and multi-token swap. Support freezable erc20 transfer
*/
contract MintSwapCanonicalTokenUpgradableFreezable is MintSwapCanonicalTokenUpgradable, Freezable {
string private _name;
string private _symbol;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) MintSwapCanonicalTokenUpgradable(name_, symbol_, decimals_) {}
// freezable related
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
require(!isFrozen(from), "ERC20Freezable: from account is frozen");
require(!isFrozen(to), "ERC20Freezable: to account is frozen");
}
function freeze(address _account) public onlyOwner {
freezes[_account] = true;
emit Frozen(_account);
}
function unfreeze(address _account) public onlyOwner {
freezes[_account] = false;
emit Unfrozen(_account);
}
}
================================================
FILE: contracts/pegged-bridge/tokens/owners/RestrictedMultiBridgeTokenOwner.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../../../safeguard/Ownable.sol";
interface IMultiBridgeToken {
function updateBridgeSupplyCap(address _bridge, uint256 _cap) external;
}
// restrict multi-bridge token to effectively only have one bridge (minter)
contract RestrictedMultiBridgeTokenOwner is Ownable {
address public immutable token;
address public bridge;
constructor(address _token, address _bridge) {
token = _token;
bridge = _bridge;
}
function updateBridgeSupplyCap(uint256 _cap) external onlyOwner {
IMultiBridgeToken(token).updateBridgeSupplyCap(bridge, _cap);
}
function changeBridge(address _bridge, uint256 _cap) external onlyOwner {
// set previous bridge cap to 1 to disable mint but still allow burn
// till its total supply becomes zero
IMultiBridgeToken(token).updateBridgeSupplyCap(bridge, 1);
// set new bridge and cap
IMultiBridgeToken(token).updateBridgeSupplyCap(_bridge, _cap);
bridge = _bridge;
}
function revokeBridge(address _bridge) external onlyOwner {
// set previous bridge cap to 0 to disable both mint and burn
IMultiBridgeToken(token).updateBridgeSupplyCap(_bridge, 0);
}
}
================================================
FILE: contracts/proxy/TransferAgent.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../libraries/BridgeTransferLib.sol";
import "../safeguard/Ownable.sol";
/**
* @title Transfer agent. Designed to support arbitrary length receiver address for transfer. Supports the liquidity pool-based {Bridge}, the {OriginalTokenVault} for pegged
* deposit and the {PeggedTokenBridge} for pegged burn.
*/
contract TransferAgent is ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;
struct Extension {
uint8 Type;
bytes Value;
}
mapping(BridgeTransferLib.BridgeSendType => address) public bridges;
event Supplement(
BridgeTransferLib.BridgeSendType bridgeSendType,
bytes32 transferId,
address sender,
bytes receiver,
Extension[] extensions
);
event BridgeUpdated(BridgeTransferLib.BridgeSendType bridgeSendType, address bridgeAddr);
/**
* @notice Send a cross-chain transfer of ERC20 token either via liquidity pool-based bridge or in form of mint/burn.
* @param _receiver The address of the receiver.
* @param _token The address of the token.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {BridgeSendType.Liquidity}.
* @param _bridgeSendType The type of bridge used by this transfer. One of the {BridgeSendType} enum.
* @param _extensions A list of extension to be processed by agent, is designed to be used for extending
* present transfer. Contact Celer team to learn about already supported type of extension.
*/
function transfer(
bytes calldata _receiver,
address _token,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage, // slippage * 1M, eg. 0.5% -> 5000
BridgeTransferLib.BridgeSendType _bridgeSendType,
Extension[] calldata _extensions
) external nonReentrant returns (bytes32) {
bytes32 transferId;
{
address _bridgeAddr = bridges[_bridgeSendType];
require(_bridgeAddr != address(0), "unknown bridge type");
IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount);
transferId = BridgeTransferLib.sendTransfer(
address(0),
_token,
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_bridgeSendType,
_bridgeAddr
);
}
emit Supplement(_bridgeSendType, transferId, msg.sender, _receiver, _extensions);
return transferId;
}
/**
* @notice Send a cross-chain transfer of native token either via liquidity pool-based bridge or in form of mint/burn.
* @param _receiver The address of the receiver.
* @param _amount The amount of the transfer.
* @param _dstChainId The destination chain ID.
* @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice.
* @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%.
* Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least
* (100% - max slippage percentage) * amount or the transfer can be refunded.
* Only applicable to the {BridgeSendType.Liquidity}.
* @param _bridgeSendType The type of bridge used by this transfer. One of the {BridgeSendType} enum.
* @param _extensions A list of extension to be processed by agent, is designed to be used for extending
* present transfer. Contact Celer team to learn about already supported type of extension.
*/
function transferNative(
bytes calldata _receiver,
uint256 _amount,
uint64 _dstChainId,
uint64 _nonce,
uint32 _maxSlippage, // slippage * 1M, eg. 0.5% -> 5000
BridgeTransferLib.BridgeSendType _bridgeSendType,
Extension[] calldata _extensions
) external payable nonReentrant returns (bytes32) {
bytes32 transferId;
{
address _bridgeAddr = bridges[_bridgeSendType];
require(_bridgeAddr != address(0), "unknown bridge type");
require(msg.value == _amount, "amount mismatch");
transferId = BridgeTransferLib.sendNativeTransfer(
address(0),
_amount,
_dstChainId,
_nonce,
_maxSlippage,
_bridgeSendType,
_bridgeAddr
);
}
emit Supplement(_bridgeSendType, transferId, msg.sender, _receiver, _extensions);
return transferId;
}
// ----------------------Admin operation-----------------------
function setBridgeAddress(BridgeTransferLib.BridgeSendType _bridgeSendType, address _addr) public onlyOwner {
require(_addr != address(0), "invalid address");
bridges[_bridgeSendType] = _addr;
emit BridgeUpdated(_bridgeSendType, _addr);
}
}
================================================
FILE: contracts/safeguard/DelayedTransfer.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Governor.sol";
abstract contract DelayedTransfer is Governor {
struct delayedTransfer {
address receiver;
address token;
uint256 amount;
uint256 timestamp;
}
mapping(bytes32 => delayedTransfer) public delayedTransfers;
mapping(address => uint256) public delayThresholds;
uint256 public delayPeriod; // in seconds
event DelayedTransferAdded(bytes32 id);
event DelayedTransferExecuted(bytes32 id, address receiver, address token, uint256 amount);
event DelayPeriodUpdated(uint256 period);
event DelayThresholdUpdated(address token, uint256 threshold);
function setDelayThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) external onlyGovernor {
require(_tokens.length == _thresholds.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
delayThresholds[_tokens[i]] = _thresholds[i];
emit DelayThresholdUpdated(_tokens[i], _thresholds[i]);
}
}
function setDelayPeriod(uint256 _period) external onlyGovernor {
delayPeriod = _period;
emit DelayPeriodUpdated(_period);
}
function _addDelayedTransfer(
bytes32 id,
address receiver,
address token,
uint256 amount
) internal {
require(delayedTransfers[id].timestamp == 0, "delayed transfer already exists");
delayedTransfers[id] = delayedTransfer({
receiver: receiver,
token: token,
amount: amount,
timestamp: block.timestamp
});
emit DelayedTransferAdded(id);
}
// caller needs to do the actual token transfer
function _executeDelayedTransfer(bytes32 id) internal returns (delayedTransfer memory) {
delayedTransfer memory transfer = delayedTransfers[id];
require(transfer.timestamp > 0, "delayed transfer not exist");
require(block.timestamp > transfer.timestamp + delayPeriod, "delayed transfer still locked");
delete delayedTransfers[id];
emit DelayedTransferExecuted(id, transfer.receiver, transfer.token, transfer.amount);
return transfer;
}
}
================================================
FILE: contracts/safeguard/Governor.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Ownable.sol";
abstract contract Governor is Ownable {
mapping(address => bool) public governors;
event GovernorAdded(address account);
event GovernorRemoved(address account);
modifier onlyGovernor() {
require(isGovernor(msg.sender), "Caller is not governor");
_;
}
constructor() {
_addGovernor(msg.sender);
}
function isGovernor(address _account) public view returns (bool) {
return governors[_account];
}
function addGovernor(address _account) public onlyOwner {
_addGovernor(_account);
}
function removeGovernor(address _account) public onlyOwner {
_removeGovernor(_account);
}
function renounceGovernor() public {
_removeGovernor(msg.sender);
}
function _addGovernor(address _account) private {
require(!isGovernor(_account), "Account is already governor");
governors[_account] = true;
emit GovernorAdded(_account);
}
function _removeGovernor(address _account) private {
require(isGovernor(_account), "Account is not governor");
governors[_account] = false;
emit GovernorRemoved(_account);
}
}
================================================
FILE: contracts/safeguard/Ownable.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*
* This adds a normal func that setOwner if _owner is address(0). So we can't allow
* renounceOwnership. So we can support Proxy based upgradable contract
*/
abstract contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
_setOwner(msg.sender);
}
/**
* @dev Only to be called by inherit contracts, in their init func called by Proxy
* we require _owner == address(0), which is only possible when it's a delegateCall
* because constructor sets _owner in contract state.
*/
function initOwner() internal {
require(_owner == address(0), "owner already set");
_setOwner(msg.sender);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(owner() == msg.sender, "Ownable: caller is not the owner");
_;
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_setOwner(newOwner);
}
function _setOwner(address newOwner) private {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
================================================
FILE: contracts/safeguard/Pauser.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/security/Pausable.sol";
import "./Ownable.sol";
abstract contract Pauser is Ownable, Pausable {
mapping(address => bool) public pausers;
event PauserAdded(address account);
event PauserRemoved(address account);
constructor() {
_addPauser(msg.sender);
}
modifier onlyPauser() {
require(isPauser(msg.sender), "Caller is not pauser");
_;
}
function pause() public onlyPauser {
_pause();
}
function unpause() public onlyPauser {
_unpause();
}
function isPauser(address account) public view returns (bool) {
return pausers[account];
}
function addPauser(address account) public onlyOwner {
_addPauser(account);
}
function removePauser(address account) public onlyOwner {
_removePauser(account);
}
function renouncePauser() public {
_removePauser(msg.sender);
}
function _addPauser(address account) private {
require(!isPauser(account), "Account is already pauser");
pausers[account] = true;
emit PauserAdded(account);
}
function _removePauser(address account) private {
require(isPauser(account), "Account is not pauser");
pausers[account] = false;
emit PauserRemoved(account);
}
}
================================================
FILE: contracts/safeguard/VolumeControl.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Governor.sol";
abstract contract VolumeControl is Governor {
uint256 public epochLength; // seconds
mapping(address => uint256) public epochVolumes; // key is token
mapping(address => uint256) public epochVolumeCaps; // key is token
mapping(address => uint256) public lastOpTimestamps; // key is token
event EpochLengthUpdated(uint256 length);
event EpochVolumeUpdated(address token, uint256 cap);
function setEpochLength(uint256 _length) external onlyGovernor {
epochLength = _length;
emit EpochLengthUpdated(_length);
}
function setEpochVolumeCaps(address[] calldata _tokens, uint256[] calldata _caps) external onlyGovernor {
require(_tokens.length == _caps.length, "length mismatch");
for (uint256 i = 0; i < _tokens.length; i++) {
epochVolumeCaps[_tokens[i]] = _caps[i];
emit EpochVolumeUpdated(_tokens[i], _caps[i]);
}
}
function _updateVolume(address _token, uint256 _amount) internal {
if (epochLength == 0) {
return;
}
uint256 cap = epochVolumeCaps[_token];
if (cap == 0) {
return;
}
uint256 volume = epochVolumes[_token];
uint256 timestamp = block.timestamp;
uint256 epochStartTime = (timestamp / epochLength) * epochLength;
if (lastOpTimestamps[_token] < epochStartTime) {
volume = _amount;
} else {
volume += _amount;
}
require(volume <= cap, "volume exceeds cap");
epochVolumes[_token] = volume;
lastOpTimestamps[_token] = timestamp;
}
}
================================================
FILE: contracts/safeguard/Whitelist.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Ownable.sol";
abstract contract Whitelist is Ownable {
mapping(address => bool) public whitelist;
bool public whitelistEnabled;
event WhitelistedAdded(address account);
event WhitelistedRemoved(address account);
modifier onlyWhitelisted() {
if (whitelistEnabled) {
require(isWhitelisted(msg.sender), "Caller is not whitelisted");
}
_;
}
/**
* @notice Set whitelistEnabled
*/
function setWhitelistEnabled(bool _whitelistEnabled) external onlyOwner {
whitelistEnabled = _whitelistEnabled;
}
/**
* @notice Add an account to whitelist
*/
function addWhitelisted(address account) external onlyOwner {
require(!isWhitelisted(account), "Already whitelisted");
whitelist[account] = true;
emit WhitelistedAdded(account);
}
/**
* @notice Remove an account from whitelist
*/
function removeWhitelisted(address account) external onlyOwner {
require(isWhitelisted(account), "Not whitelisted");
whitelist[account] = false;
emit WhitelistedRemoved(account);
}
/**
* @return is account whitelisted
*/
function isWhitelisted(address account) public view returns (bool) {
return whitelist[account];
}
}
================================================
FILE: contracts/safeguard/sentinel/Guard.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "../Ownable.sol";
abstract contract Guard is Ownable {
enum GuardState {
None,
Guarded,
Relaxed
}
bool public relaxed;
uint256 public numRelaxedGuards;
uint256 public relaxThreshold;
address[] public guards;
mapping(address => GuardState) public guardStates; // guard address -> guard state
event GuardUpdated(address account, GuardState state);
event RelaxStatusUpdated(bool relaxed);
event RelaxThresholdUpdated(uint256 threshold, uint256 total);
function _initGuards(address[] memory _guards) internal {
require(guards.length == 0, "guards already initiated");
for (uint256 i = 0; i < _guards.length; i++) {
_addGuard(_guards[i]);
}
_setRelaxThreshold(guards.length);
}
// change GuardState of msg.sender from relaxed to guarded
function guard() external {
require(guardStates[msg.sender] == GuardState.Relaxed, "invalid caller");
guardStates[msg.sender] = GuardState.Guarded;
numRelaxedGuards--;
_updateRelaxed();
emit GuardUpdated(msg.sender, GuardState.Guarded);
}
// change GuardState of msg.sender from guarded to relaxed
function relax() external {
require(guardStates[msg.sender] == GuardState.Guarded, "invalid caller");
guardStates[msg.sender] = GuardState.Relaxed;
numRelaxedGuards++;
_updateRelaxed();
emit GuardUpdated(msg.sender, GuardState.Relaxed);
}
function updateGuards(
address[] calldata _add,
address[] calldata _remove,
uint256 _newRelaxThreshold
) external onlyOwner {
for (uint256 i = 0; i < _remove.length; i++) {
_removeGuard(_remove[i]);
}
for (uint256 i = 0; i < _add.length; i++) {
_addGuard(_add[i]);
}
_setRelaxThreshold(_newRelaxThreshold);
}
function _addGuard(address _account) private {
require(guardStates[_account] == GuardState.None, "account is already guard");
guards.push(_account);
guardStates[_account] = GuardState.Guarded;
emit GuardUpdated(_account, GuardState.Guarded);
}
function _removeGuard(address _account) private {
GuardState state = guardStates[_account];
require(state != GuardState.None, "account is not guard");
if (state == GuardState.Relaxed) {
numRelaxedGuards--;
}
uint256 lastIndex = guards.length - 1;
for (uint256 i = 0; i < guards.length; i++) {
if (guards[i] == _account) {
if (i < lastIndex) {
guards[i] = guards[lastIndex];
}
guards.pop();
guardStates[_account] = GuardState.None;
emit GuardUpdated(_account, GuardState.None);
return;
}
}
revert("guard not found"); // this should never happen
}
function setRelaxThreshold(uint256 _threshold) external onlyOwner {
_setRelaxThreshold(_threshold);
}
function _setRelaxThreshold(uint256 _threshold) private {
require(_threshold <= guards.length, "invalid threshold");
relaxThreshold = _threshold;
_updateRelaxed();
emit RelaxThresholdUpdated(_threshold, guards.length);
}
function _updateRelaxed() private {
bool _relaxed = numRelaxedGuards >= relaxThreshold;
if (relaxed != _relaxed) {
relaxed = _relaxed;
emit RelaxStatusUpdated(relaxed);
}
}
function numGuards() public view returns (uint256) {
return guards.length;
}
}
================================================
FILE: contracts/safeguard/sentinel/GuardedGovernor.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Guard.sol";
interface IBridge {
// delayed transfer
function setDelayPeriod(uint256 _period) external;
function delayPeriod() external view returns (uint256);
function setDelayThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) external;
function delayThresholds(address _token) external view returns (uint256);
// volume control
function setEpochLength(uint256 _length) external;
function epochLength() external view returns (uint256);
function setEpochVolumeCaps(address[] calldata _tokens, uint256[] calldata _caps) external;
function epochVolumeCaps(address _token) external view returns (uint256);
// pool bridge
function setMinAdd(address[] calldata _tokens, uint256[] calldata _amounts) external;
function minAdd(address _token) external view returns (uint256);
function setMinSend(address[] calldata _tokens, uint256[] calldata _amounts) external;
function minSend(address _token) external view returns (uint256);
function setMaxSend(address[] calldata _tokens, uint256[] calldata _amounts) external;
function maxSend(address _token) external view returns (uint256);
function setNativeTokenTransferGas(uint256 _gasUsed) external;
function setMinimalMaxSlippage(uint32 _minimalMaxSlippage) external;
// peg bridge
function setMinDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external;
function minDeposit(address _token) external view returns (uint256);
function setMaxDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external;
function maxDeposit(address _token) external view returns (uint256);
function setMinBurn(address[] calldata _tokens, uint256[] calldata _amounts) external;
function minBurn(address _token) external view returns (uint256);
function setMaxBurn(address[] calldata _tokens, uint256[] calldata _amounts) external;
function maxBurn(address _token) external view returns (uint256);
}
abstract contract GuardedGovernor is Guard {
uint64 public numGovernors;
mapping(address => bool) public governors;
event GovernorUpdated(address account, bool added);
function _initGovernors(address[] memory _governors) internal {
require(numGovernors == 0, "governors already initiated");
for (uint256 i = 0; i < _governors.length; i++) {
_addGovernor(_governors[i]);
}
}
modifier onlyGovernor() {
require(isGovernor(msg.sender), "Caller is not governor");
_;
}
// delayed transfer
function setDelayPeriod(address _target, uint256 _period) external onlyGovernor {
if (!relaxed) {
uint256 current = IBridge(_target).delayPeriod();
require(_period > current, "not in relax mode, can only increase period");
}
IBridge(_target).setDelayPeriod(_period);
}
function setDelayThresholds(
address _target,
address[] calldata _tokens,
uint256[] calldata _thresholds
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).delayThresholds(_tokens[i]);
require(_thresholds[i] > current, "not in relax mode, can only increase threshold");
}
}
IBridge(_target).setDelayThresholds(_tokens, _thresholds);
}
// volume control
function setEpochLength(address _target, uint256 _length) external onlyGovernor {
if (!relaxed) {
uint256 current = IBridge(_target).epochLength();
require(_length > current, "not in relax mode, can only increase length");
}
IBridge(_target).setEpochLength(_length);
}
function setEpochVolumeCaps(
address _target,
address[] calldata _tokens,
uint256[] calldata _caps
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).epochVolumeCaps(_tokens[i]);
require(_caps[i] < current, "not in relax mode, can only reduce cap");
}
}
IBridge(_target).setEpochVolumeCaps(_tokens, _caps);
}
// pool bridge
function setMinAdd(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).minAdd(_tokens[i]);
require(_amounts[i] > current, "not in relax mode, can only increase minAdd");
}
}
IBridge(_target).setMinAdd(_tokens, _amounts);
}
function setMinSend(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).minSend(_tokens[i]);
require(_amounts[i] > current, "not in relax mode, can only increase minSend");
}
}
IBridge(_target).setMinSend(_tokens, _amounts);
}
function setMaxSend(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).maxSend(_tokens[i]);
require(_amounts[i] < current, "not in relax mode, can only reduce maxSend");
}
}
IBridge(_target).setMaxSend(_tokens, _amounts);
}
function setNativeTokenTransferGas(address _target, uint256 _gasUsed) external onlyGovernor {
IBridge(_target).setNativeTokenTransferGas(_gasUsed);
}
function setMinimalMaxSlippage(address _target, uint32 _minimalMaxSlippage) external onlyGovernor {
IBridge(_target).setMinimalMaxSlippage(_minimalMaxSlippage);
}
// peg bridge
function setMinDeposit(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).minDeposit(_tokens[i]);
require(_amounts[i] > current, "not in relax mode, can only increase minDeposit");
}
}
IBridge(_target).setMinDeposit(_tokens, _amounts);
}
function setMaxDeposit(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).maxDeposit(_tokens[i]);
require(_amounts[i] < current, "not in relax mode, can only reduce maxDeposit");
}
}
IBridge(_target).setMaxDeposit(_tokens, _amounts);
}
function setMinBurn(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).minBurn(_tokens[i]);
require(_amounts[i] > current, "not in relax mode, can only increase minBurn");
}
}
IBridge(_target).setMinBurn(_tokens, _amounts);
}
function setMaxBurn(
address _target,
address[] calldata _tokens,
uint256[] calldata _amounts
) external onlyGovernor {
if (!relaxed) {
for (uint256 i = 0; i < _tokens.length; i++) {
uint256 current = IBridge(_target).maxBurn(_tokens[i]);
require(_amounts[i] < current, "not in relax mode, can only reduce maxBurn");
}
}
IBridge(_target).setMaxBurn(_tokens, _amounts);
}
function isGovernor(address _account) public view returns (bool) {
return governors[_account];
}
function addGovernors(address[] calldata _accounts) external onlyOwner {
for (uint256 i = 0; i < _accounts.length; i++) {
_addGovernor(_accounts[i]);
}
}
function _addGovernor(address _account) internal {
require(!isGovernor(_account), "Account is already governor");
governors[_account] = true;
numGovernors++;
emit GovernorUpdated(_account, true);
}
function removeGovernors(address[] calldata _accounts) external onlyOwner {
for (uint256 i = 0; i < _accounts.length; i++) {
_removeGovernor(_accounts[i]);
}
}
function _removeGovernor(address _account) private {
require(isGovernor(_account), "Account is not governor");
governors[_account] = false;
numGovernors--;
emit GovernorUpdated(_account, false);
}
function renounceGovernor() external {
_removeGovernor(msg.sender);
}
}
================================================
FILE: contracts/safeguard/sentinel/GuardedPauser.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./Guard.sol";
import "../../libraries/Utils.sol";
interface IPauser {
function pause() external;
function unpause() external;
}
abstract contract GuardedPauser is Guard {
enum PauserRole {
None,
Full,
PauseOnly
}
uint64 public numPausers;
mapping(address => PauserRole) public pausers;
event PauserUpdated(address account, PauserRole role);
event Failed(address target, string reason);
function _initPausers(address[] memory _pausers) internal {
require(numPausers == 0, "pausers already initiated");
for (uint256 i = 0; i < _pausers.length; i++) {
_addPauser(_pausers[i], PauserRole.Full);
}
}
function pause(address _target) public {
require(pausers[msg.sender] != PauserRole.None, "invalid caller");
IPauser(_target).pause();
}
function pause(address[] calldata _targets) public {
require(pausers[msg.sender] != PauserRole.None, "invalid caller");
require(_targets.length > 0, "empty target list");
bool hasSuccess;
for (uint256 i = 0; i < _targets.length; i++) {
(bool ok, bytes memory res) = address(_targets[i]).call(abi.encodeWithSelector(IPauser.pause.selector));
if (ok) {
hasSuccess = true;
} else {
emit Failed(_targets[i], Utils.getRevertMsg(res));
}
}
require(hasSuccess, "pause failed for all targets");
}
function unpause(address _target) public {
require(pausers[msg.sender] == PauserRole.Full, "invalid caller");
require(relaxed, "not in relaxed mode");
IPauser(_target).unpause();
}
function unpause(address[] calldata _targets) public {
require(pausers[msg.sender] == PauserRole.Full, "invalid caller");
require(relaxed, "not in relaxed mode");
require(_targets.length > 0, "empty target list");
bool hasSuccess;
for (uint256 i = 0; i < _targets.length; i++) {
(bool ok, bytes memory res) = address(_targets[i]).call(abi.encodeWithSelector(IPauser.unpause.selector));
if (ok) {
hasSuccess = true;
} else {
emit Failed(_targets[i], Utils.getRevertMsg(res));
}
}
require(hasSuccess, "unpause failed for all targets");
}
function addPausers(address[] calldata _accounts, PauserRole[] calldata _roles) external onlyOwner {
for (uint256 i = 0; i < _accounts.length; i++) {
_addPauser(_accounts[i], _roles[i]);
}
}
function _addPauser(address _account, PauserRole _role) private {
require(pausers[_account] == PauserRole.None, "account is already pauser");
require(_role == PauserRole.Full || _role == PauserRole.PauseOnly, "invalid role");
pausers[_account] = _role;
numPausers++;
emit PauserUpdated(_account, _role);
}
function removePausers(address[] calldata _accounts) external onlyOwner {
for (uint256 i = 0; i < _accounts.length; i++) {
_removePauser(_accounts[i]);
}
}
function _removePauser(address _account) private {
require(pausers[_account] != PauserRole.None, "account is not pauser");
pausers[_account] = PauserRole.None;
numPausers--;
emit PauserUpdated(_account, PauserRole.None);
}
function setPausers(address[] calldata _accounts, PauserRole[] calldata _roles) external onlyOwner {
for (uint256 i = 0; i < _accounts.length; i++) {
_setPauser(_accounts[i], _roles[i]);
}
}
function _setPauser(address _account, PauserRole _role) private {
require(pausers[_account] != PauserRole.None, "account is not pauser");
require(_role == PauserRole.Full || _role == PauserRole.PauseOnly, "invalid role");
pausers[_account] = _role;
emit PauserUpdated(_account, _role);
}
}
================================================
FILE: contracts/safeguard/sentinel/Sentinel.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "./GuardedPauser.sol";
import "./GuardedGovernor.sol";
contract Sentinel is GuardedPauser, GuardedGovernor {
// NOTE: Comment out for zksync
constructor(address[] memory _guards, address[] memory _pausers, address[] memory _governors) {
_initGuards(_guards);
_initPausers(_pausers);
_initGovernors(_governors);
}
// This is to support upgradable deployment.
// Only to be called by Proxy via delegateCall as initOwner will require _owner is 0,
// so calling init on this contract directly will guarantee to fail
function init(address[] memory _guards, address[] memory _pausers, address[] memory _governors) external {
initOwner();
_initGuards(_guards);
_initPausers(_pausers);
_initGovernors(_governors);
}
}
================================================
FILE: contracts/staking/DataTypes.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
library DataTypes {
uint256 constant CELR_DECIMAL = 1e18;
uint256 constant MAX_INT = 2**256 - 1;
uint256 constant COMMISSION_RATE_BASE = 10000; // 1 commissionRate means 0.01%
uint256 constant MAX_UNDELEGATION_ENTRIES = 10;
uint256 constant SLASH_FACTOR_DECIMAL = 1e6;
enum ValidatorStatus {
Null,
Unbonded,
Unbonding,
Bonded
}
enum ParamName {
ProposalDeposit,
VotingPeriod,
UnbondingPeriod,
MaxBondedValidators,
MinValidatorTokens,
MinSelfDelegation,
AdvanceNoticePeriod,
ValidatorBondInterval,
MaxSlashFactor
}
struct Undelegation {
uint256 shares;
uint256 creationBlock;
}
struct Undelegations {
mapping(uint256 => Undelegation) queue;
uint32 head;
uint32 tail;
}
struct Delegator {
uint256 shares;
Undelegations undelegations;
}
struct Validator {
ValidatorStatus status;
address signer;
uint256 tokens; // sum of all tokens delegated to this validator
uint256 shares; // sum of all delegation shares
uint256 undelegationTokens; // tokens being undelegated
uint256 undelegationShares; // shares of tokens being undelegated
mapping(address => Delegator) delegators;
uint256 minSelfDelegation;
uint64 bondBlock; // cannot become bonded before this block
uint64 unbondBlock; // cannot become unbonded before this block
uint64 commissionRate; // equal to real commission rate * COMMISSION_RATE_BASE
}
// used for external view output
struct ValidatorTokens {
address valAddr;
uint256 tokens;
}
// used for external view output
struct ValidatorInfo {
address valAddr;
ValidatorStatus status;
address signer;
uint256 tokens;
uint256 shares;
uint256 minSelfDelegation;
uint64 commissionRate;
}
// used for external view output
struct DelegatorInfo {
address valAddr;
uint256 tokens;
uint256 shares;
Undelegation[] undelegations;
uint256 undelegationTokens;
uint256 withdrawableUndelegationTokens;
}
}
================================================
FILE: contracts/staking/Govern.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {DataTypes as dt} from "./DataTypes.sol";
import "./Staking.sol";
/**
* @title Governance module for Staking contract
*/
contract Govern {
using SafeERC20 for IERC20;
Staking public immutable staking;
IERC20 public immutable celerToken;
enum ProposalStatus {
Uninitiated,
Voting,
Closed
}
enum VoteOption {
Null,
Yes,
Abstain,
No
}
struct ParamProposal {
address proposer;
uint256 deposit;
uint256 voteDeadline;
dt.ParamName name;
uint256 newValue;
ProposalStatus status;
mapping(address => VoteOption) votes;
}
mapping(uint256 => ParamProposal) public paramProposals;
uint256 public nextParamProposalId;
uint256 public forfeiture;
address public immutable collector;
event CreateParamProposal(
uint256 proposalId,
address proposer,
uint256 deposit,
uint256 voteDeadline,
dt.ParamName name,
uint256 newValue
);
event VoteParam(uint256 proposalId, address voter, VoteOption vote);
event ConfirmParamProposal(uint256 proposalId, bool passed, dt.ParamName name, uint256 newValue);
constructor(
Staking _staking,
address _celerTokenAddress,
address _collector
) {
staking = _staking;
celerToken = IERC20(_celerTokenAddress);
collector = _collector;
}
/**
* @notice Get the vote type of a voter on a parameter proposal
* @param _proposalId the proposal id
* @param _voter the voter address
* @return the vote type of the given voter on the given parameter proposal
*/
function getParamProposalVote(uint256 _proposalId, address _voter) public view returns (VoteOption) {
return paramProposals[_proposalId].votes[_voter];
}
/**
* @notice Create a parameter proposal
* @param _name the key of this parameter
* @param _value the new proposed value of this parameter
*/
function createParamProposal(dt.ParamName _name, uint256 _value) external {
ParamProposal storage p = paramProposals[nextParamProposalId];
nextParamProposalId = nextParamProposalId + 1;
address msgSender = msg.sender;
uint256 deposit = staking.getParamValue(dt.ParamName.ProposalDeposit);
p.proposer = msgSender;
p.deposit = deposit;
p.voteDeadline = block.number + staking.getParamValue(dt.ParamName.VotingPeriod);
p.name = _name;
p.newValue = _value;
p.status = ProposalStatus.Voting;
celerToken.safeTransferFrom(msgSender, address(this), deposit);
emit CreateParamProposal(nextParamProposalId - 1, msgSender, deposit, p.voteDeadline, _name, _value);
}
/**
* @notice Vote for a parameter proposal with a specific type of vote
* @param _proposalId the id of the parameter proposal
* @param _vote the type of vote
*/
function voteParam(uint256 _proposalId, VoteOption _vote) external {
address valAddr = msg.sender;
require(staking.getValidatorStatus(valAddr) == dt.ValidatorStatus.Bonded, "Voter is not a bonded validator");
ParamProposal storage p = paramProposals[_proposalId];
require(p.status == ProposalStatus.Voting, "Invalid proposal status");
require(block.number < p.voteDeadline, "Vote deadline passed");
require(p.votes[valAddr] == VoteOption.Null, "Voter has voted");
require(_vote != VoteOption.Null, "Invalid vote");
p.votes[valAddr] = _vote;
emit VoteParam(_proposalId, valAddr, _vote);
}
/**
* @notice Confirm a parameter proposal
* @param _proposalId the id of the parameter proposal
*/
function confirmParamProposal(uint256 _proposalId) external {
uint256 yesVotes;
uint256 bondedTokens;
dt.ValidatorTokens[] memory validators = staking.getBondedValidatorsTokens();
for (uint32 i = 0; i < validators.length; i++) {
if (getParamProposalVote(_proposalId, validators[i].valAddr) == VoteOption.Yes) {
yesVotes += validators[i].tokens;
}
bondedTokens += validators[i].tokens;
}
bool passed = (yesVotes >= (bondedTokens * 2) / 3 + 1);
ParamProposal storage p = paramProposals[_proposalId];
require(p.status == ProposalStatus.Voting, "Invalid proposal status");
require(block.number >= p.voteDeadline, "Vote deadline not reached");
p.status = ProposalStatus.Closed;
if (passed) {
staking.setParamValue(p.name, p.newValue);
celerToken.safeTransfer(p.proposer, p.deposit);
} else {
forfeiture += p.deposit;
}
emit ConfirmParamProposal(_proposalId, passed, p.name, p.newValue);
}
function collectForfeiture() external {
require(forfeiture > 0, "Nothing to collect");
celerToken.safeTransfer(collector, forfeiture);
forfeiture = 0;
}
}
================================================
FILE: contracts/staking/SGN.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {DataTypes as dt} from "./DataTypes.sol";
import "../libraries/PbSgn.sol";
import "../safeguard/Pauser.sol";
import "./Staking.sol";
/**
* @title contract of SGN chain
*/
contract SGN is Pauser {
using SafeERC20 for IERC20;
Staking public immutable staking;
bytes32[] public deposits;
// account -> (token -> amount)
mapping(address => mapping(address => uint256)) public withdrawnAmts;
mapping(address => bytes) public sgnAddrs;
/* Events */
event SgnAddrUpdate(address indexed valAddr, bytes oldAddr, bytes newAddr);
event Deposit(uint256 depositId, address account, address token, uint256 amount);
event Withdraw(address account, address token, uint256 amount);
/**
* @notice SGN constructor
* @dev Need to deploy Staking contract first before deploying SGN contract
* @param _staking address of Staking Contract
*/
constructor(Staking _staking) {
staking = _staking;
}
/**
* @notice Update sgn address
* @param _sgnAddr the new address in the layer 2 SGN
*/
function updateSgnAddr(bytes calldata _sgnAddr) external {
address valAddr = msg.sender;
if (staking.signerVals(msg.sender) != address(0)) {
valAddr = staking.signerVals(msg.sender);
}
dt.ValidatorStatus status = staking.getValidatorStatus(valAddr);
require(status == dt.ValidatorStatus.Unbonded, "Not unbonded validator");
bytes memory oldAddr = sgnAddrs[valAddr];
sgnAddrs[valAddr] = _sgnAddr;
staking.validatorNotice(valAddr, "sgn-addr", _sgnAddr);
emit SgnAddrUpdate(valAddr, oldAddr, _sgnAddr);
}
/**a
* @notice Deposit to SGN
* @param _amount subscription fee paid along this function call in CELR tokens
*/
function deposit(address _token, uint256 _amount) external whenNotPaused {
address msgSender = msg.sender;
deposits.push(keccak256(abi.encodePacked(msgSender, _token, _amount)));
IERC20(_token).safeTransferFrom(msgSender, address(this), _amount);
uint64 depositId = uint64(deposits.length - 1);
emit Deposit(depositId, msgSender, _token, _amount);
}
/**
* @notice Withdraw token
* @dev Here we use cumulative amount to make withdrawal process idempotent
* @param _withdrawalRequest withdrawal request bytes coded in protobuf
* @param _sigs list of validator signatures
*/
function withdraw(bytes calldata _withdrawalRequest, bytes[] calldata _sigs) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Withdrawal"));
staking.verifySignatures(abi.encodePacked(domain, _withdrawalRequest), _sigs);
PbSgn.Withdrawal memory withdrawal = PbSgn.decWithdrawal(_withdrawalRequest);
uint256 amount = withdrawal.cumulativeAmount - withdrawnAmts[withdrawal.account][withdrawal.token];
require(amount > 0, "No new amount to withdraw");
withdrawnAmts[withdrawal.account][withdrawal.token] = withdrawal.cumulativeAmount;
IERC20(withdrawal.token).safeTransfer(withdrawal.account, amount);
emit Withdraw(withdrawal.account, withdrawal.token, amount);
}
/**
* @notice Owner drains one type of tokens when the contract is paused
* @dev emergency use only
* @param _amount drained token amount
*/
function drainToken(address _token, uint256 _amount) external whenPaused onlyOwner {
IERC20(_token).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/staking/Staking.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {DataTypes as dt} from "./DataTypes.sol";
import "../interfaces/ISigsVerifier.sol";
import "../libraries/PbStaking.sol";
import "../safeguard/Pauser.sol";
import "../safeguard/Whitelist.sol";
/**
* @title A Staking contract shared by all external sidechains and apps
*/
contract Staking is ISigsVerifier, Pauser, Whitelist {
using SafeERC20 for IERC20;
using ECDSA for bytes32;
IERC20 public immutable CELER_TOKEN;
uint256 public bondedTokens;
uint256 public nextBondBlock;
address[] public valAddrs;
address[] public bondedValAddrs;
mapping(address => dt.Validator) public validators; // key is valAddr
mapping(address => address) public signerVals; // signerAddr -> valAddr
mapping(uint256 => bool) public slashNonces;
mapping(dt.ParamName => uint256) public params;
address public govContract;
address public rewardContract;
uint256 public forfeiture;
/* Events */
event ValidatorNotice(address indexed valAddr, string key, bytes data, address from);
event ValidatorStatusUpdate(address indexed valAddr, dt.ValidatorStatus indexed status);
event DelegationUpdate(
address indexed valAddr,
address indexed delAddr,
uint256 valTokens,
uint256 delShares,
int256 tokenDiff
);
event Undelegated(address indexed valAddr, address indexed delAddr, uint256 amount);
event Slash(address indexed valAddr, uint64 nonce, uint256 slashAmt);
event SlashAmtCollected(address indexed recipient, uint256 amount);
/**
* @notice Staking constructor
* @param _celerTokenAddress address of Celer Token Contract
* @param _proposalDeposit required deposit amount for a governance proposal
* @param _votingPeriod voting timeout for a governance proposal
* @param _unbondingPeriod the locking time for funds locked before withdrawn
* @param _maxBondedValidators the maximum number of bonded validators
* @param _minValidatorTokens the global minimum token amount requirement for bonded validator
* @param _minSelfDelegation minimal amount of self-delegated tokens
* @param _advanceNoticePeriod the wait time after the announcement and prior to the effective date of an update
* @param _validatorBondInterval min interval between bondValidator
* @param _maxSlashFactor maximal slashing factor (1e6 = 100%)
*/
constructor(
address _celerTokenAddress,
uint256 _proposalDeposit,
uint256 _votingPeriod,
uint256 _unbondingPeriod,
uint256 _maxBondedValidators,
uint256 _minValidatorTokens,
uint256 _minSelfDelegation,
uint256 _advanceNoticePeriod,
uint256 _validatorBondInterval,
uint256 _maxSlashFactor
) {
CELER_TOKEN = IERC20(_celerTokenAddress);
params[dt.ParamName.ProposalDeposit] = _proposalDeposit;
params[dt.ParamName.VotingPeriod] = _votingPeriod;
params[dt.ParamName.UnbondingPeriod] = _unbondingPeriod;
params[dt.ParamName.MaxBondedValidators] = _maxBondedValidators;
params[dt.ParamName.MinValidatorTokens] = _minValidatorTokens;
params[dt.ParamName.MinSelfDelegation] = _minSelfDelegation;
params[dt.ParamName.AdvanceNoticePeriod] = _advanceNoticePeriod;
params[dt.ParamName.ValidatorBondInterval] = _validatorBondInterval;
params[dt.ParamName.MaxSlashFactor] = _maxSlashFactor;
}
receive() external payable {}
/*********************************
* External and Public Functions *
*********************************/
/**
* @notice Initialize a validator candidate
* @param _signer signer address
* @param _minSelfDelegation minimal amount of tokens staked by the validator itself
* @param _commissionRate the self-declaimed commission rate
*/
function initializeValidator(
address _signer,
uint256 _minSelfDelegation,
uint64 _commissionRate
) external whenNotPaused onlyWhitelisted {
address valAddr = msg.sender;
dt.Validator storage validator = validators[valAddr];
require(validator.status == dt.ValidatorStatus.Null, "Validator is initialized");
require(validators[_signer].status == dt.ValidatorStatus.Null, "Signer is other validator");
require(signerVals[valAddr] == address(0), "Validator is other signer");
require(signerVals[_signer] == address(0), "Signer already used");
require(_commissionRate <= dt.COMMISSION_RATE_BASE, "Invalid commission rate");
require(_minSelfDelegation >= params[dt.ParamName.MinSelfDelegation], "Insufficient min self delegation");
validator.signer = _signer;
validator.status = dt.ValidatorStatus.Unbonded;
validator.minSelfDelegation = _minSelfDelegation;
validator.commissionRate = _commissionRate;
valAddrs.push(valAddr);
signerVals[_signer] = valAddr;
delegate(valAddr, _minSelfDelegation);
emit ValidatorNotice(valAddr, "init", abi.encode(_signer, _minSelfDelegation, _commissionRate), address(0));
}
/**
* @notice Update validator signer address
* @param _signer signer address
*/
function updateValidatorSigner(address _signer) external {
address valAddr = msg.sender;
dt.Validator storage validator = validators[valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator not initialized");
require(signerVals[_signer] == address(0), "Signer already used");
if (_signer != valAddr) {
require(validators[_signer].status == dt.ValidatorStatus.Null, "Signer is other validator");
}
delete signerVals[validator.signer];
validator.signer = _signer;
signerVals[_signer] = valAddr;
emit ValidatorNotice(valAddr, "signer", abi.encode(_signer), address(0));
}
/**
* @notice Candidate claims to become a bonded validator
* @dev caller can be either validator owner or signer
*/
function bondValidator() external {
address valAddr = msg.sender;
if (signerVals[msg.sender] != address(0)) {
valAddr = signerVals[msg.sender];
}
dt.Validator storage validator = validators[valAddr];
require(
validator.status == dt.ValidatorStatus.Unbonded || validator.status == dt.ValidatorStatus.Unbonding,
"Invalid validator status"
);
require(block.number >= validator.bondBlock, "Bond block not reached");
require(block.number >= nextBondBlock, "Too frequent validator bond");
nextBondBlock = block.number + params[dt.ParamName.ValidatorBondInterval];
require(hasMinRequiredTokens(valAddr, true), "Not have min tokens");
uint256 maxBondedValidators = params[dt.ParamName.MaxBondedValidators];
// if the number of validators has not reached the max_validator_num,
// add validator directly
if (bondedValAddrs.length < maxBondedValidators) {
_bondValidator(valAddr);
_decentralizationCheck(validator.tokens);
return;
}
// if the number of validators has already reached the max_validator_num,
// add validator only if its tokens is more than the current least bonded validator tokens
uint256 minTokens = dt.MAX_INT;
uint256 minTokensIndex;
for (uint256 i = 0; i < maxBondedValidators; i++) {
if (validators[bondedValAddrs[i]].tokens < minTokens) {
minTokensIndex = i;
minTokens = validators[bondedValAddrs[i]].tokens;
if (minTokens == 0) {
break;
}
}
}
require(validator.tokens > minTokens, "Insufficient tokens");
_replaceBondedValidator(valAddr, minTokensIndex);
_decentralizationCheck(validator.tokens);
}
/**
* @notice Confirm validator status from Unbonding to Unbonded
* @param _valAddr the address of the validator
*/
function confirmUnbondedValidator(address _valAddr) external {
dt.Validator storage validator = validators[_valAddr];
require(validator.status == dt.ValidatorStatus.Unbonding, "Validator not unbonding");
require(block.number >= validator.unbondBlock, "Unbond block not reached");
validator.status = dt.ValidatorStatus.Unbonded;
delete validator.unbondBlock;
emit ValidatorStatusUpdate(_valAddr, dt.ValidatorStatus.Unbonded);
}
/**
* @notice Delegate CELR tokens to a validator
* @dev Minimal amount per delegate operation is 1 CELR
* @param _valAddr validator to delegate
* @param _tokens the amount of delegated CELR tokens
*/
function delegate(address _valAddr, uint256 _tokens) public whenNotPaused {
address delAddr = msg.sender;
require(_tokens >= dt.CELR_DECIMAL, "Minimal amount is 1 CELR");
dt.Validator storage validator = validators[_valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
uint256 shares = _tokenToShare(_tokens, validator.tokens, validator.shares);
dt.Delegator storage delegator = validator.delegators[delAddr];
delegator.shares += shares;
validator.shares += shares;
validator.tokens += _tokens;
if (validator.status == dt.ValidatorStatus.Bonded) {
bondedTokens += _tokens;
_decentralizationCheck(validator.tokens);
}
CELER_TOKEN.safeTransferFrom(delAddr, address(this), _tokens);
emit DelegationUpdate(_valAddr, delAddr, validator.tokens, delegator.shares, int256(_tokens));
}
/**
* @notice Undelegate shares from a validator
* @dev Tokens are delegated by the msgSender to the validator
* @param _valAddr the address of the validator
* @param _shares undelegate shares
*/
function undelegateShares(address _valAddr, uint256 _shares) external {
require(_shares >= dt.CELR_DECIMAL, "Minimal amount is 1 share");
dt.Validator storage validator = validators[_valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
uint256 tokens = _shareToToken(_shares, validator.tokens, validator.shares);
_undelegate(validator, _valAddr, tokens, _shares);
}
/**
* @notice Undelegate shares from a validator
* @dev Tokens are delegated by the msgSender to the validator
* @param _valAddr the address of the validator
* @param _tokens undelegate tokens
*/
function undelegateTokens(address _valAddr, uint256 _tokens) external {
require(_tokens >= dt.CELR_DECIMAL, "Minimal amount is 1 CELR");
dt.Validator storage validator = validators[_valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
uint256 shares = _tokenToShare(_tokens, validator.tokens, validator.shares);
_undelegate(validator, _valAddr, _tokens, shares);
}
/**
* @notice Complete pending undelegations from a validator
* @param _valAddr the address of the validator
*/
function completeUndelegate(address _valAddr) external {
address delAddr = msg.sender;
dt.Validator storage validator = validators[_valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
dt.Delegator storage delegator = validator.delegators[delAddr];
uint256 unbondingPeriod = params[dt.ParamName.UnbondingPeriod];
bool isUnbonded = validator.status == dt.ValidatorStatus.Unbonded;
// for all pending undelegations
uint32 i;
uint256 undelegationShares;
for (i = delegator.undelegations.head; i < delegator.undelegations.tail; i++) {
if (isUnbonded || delegator.undelegations.queue[i].creationBlock + unbondingPeriod <= block.number) {
// complete undelegation when the validator becomes unbonded or
// the unbondingPeriod for the pending undelegation is up.
undelegationShares += delegator.undelegations.queue[i].shares;
delete delegator.undelegations.queue[i];
continue;
}
break;
}
delegator.undelegations.head = i;
require(undelegationShares > 0, "No undelegation ready to be completed");
uint256 tokens = _shareToToken(undelegationShares, validator.undelegationTokens, validator.undelegationShares);
validator.undelegationShares -= undelegationShares;
validator.undelegationTokens -= tokens;
CELER_TOKEN.safeTransfer(delAddr, tokens);
emit Undelegated(_valAddr, delAddr, tokens);
}
/**
* @notice Update commission rate
* @param _newRate new commission rate
*/
function updateCommissionRate(uint64 _newRate) external {
address valAddr = msg.sender;
dt.Validator storage validator = validators[valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
require(_newRate <= dt.COMMISSION_RATE_BASE, "Invalid new rate");
validator.commissionRate = _newRate;
emit ValidatorNotice(valAddr, "commission", abi.encode(_newRate), address(0));
}
/**
* @notice Update minimal self delegation value
* @param _minSelfDelegation minimal amount of tokens staked by the validator itself
*/
function updateMinSelfDelegation(uint256 _minSelfDelegation) external {
address valAddr = msg.sender;
dt.Validator storage validator = validators[valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
require(_minSelfDelegation >= params[dt.ParamName.MinSelfDelegation], "Insufficient min self delegation");
if (_minSelfDelegation < validator.minSelfDelegation) {
require(validator.status != dt.ValidatorStatus.Bonded, "Validator is bonded");
validator.bondBlock = uint64(block.number + params[dt.ParamName.AdvanceNoticePeriod]);
}
validator.minSelfDelegation = _minSelfDelegation;
emit ValidatorNotice(valAddr, "min-self-delegation", abi.encode(_minSelfDelegation), address(0));
}
/**
* @notice Slash a validator and its delegators
* @param _slashRequest slash request bytes coded in protobuf
* @param _sigs list of validator signatures
*/
function slash(bytes calldata _slashRequest, bytes[] calldata _sigs) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Slash"));
verifySignatures(abi.encodePacked(domain, _slashRequest), _sigs);
PbStaking.Slash memory request = PbStaking.decSlash(_slashRequest);
require(block.timestamp < request.expireTime, "Slash expired");
require(request.slashFactor <= dt.SLASH_FACTOR_DECIMAL, "Invalid slash factor");
require(request.slashFactor <= params[dt.ParamName.MaxSlashFactor], "Exceed max slash factor");
require(!slashNonces[request.nonce], "Used slash nonce");
slashNonces[request.nonce] = true;
address valAddr = request.validator;
dt.Validator storage validator = validators[valAddr];
require(
validator.status == dt.ValidatorStatus.Bonded || validator.status == dt.ValidatorStatus.Unbonding,
"Invalid validator status"
);
// slash delegated tokens
uint256 slashAmt = (validator.tokens * request.slashFactor) / dt.SLASH_FACTOR_DECIMAL;
validator.tokens -= slashAmt;
if (validator.status == dt.ValidatorStatus.Bonded) {
bondedTokens -= slashAmt;
if (request.jailPeriod > 0 || !hasMinRequiredTokens(valAddr, true)) {
_unbondValidator(valAddr);
}
}
if (validator.status == dt.ValidatorStatus.Unbonding && request.jailPeriod > 0) {
validator.bondBlock = uint64(block.number + request.jailPeriod);
}
emit DelegationUpdate(valAddr, address(0), validator.tokens, 0, -int256(slashAmt));
// slash pending undelegations
uint256 slashUndelegation = (validator.undelegationTokens * request.slashFactor) / dt.SLASH_FACTOR_DECIMAL;
validator.undelegationTokens -= slashUndelegation;
slashAmt += slashUndelegation;
uint256 collectAmt;
for (uint256 i = 0; i < request.collectors.length; i++) {
PbStaking.AcctAmtPair memory collector = request.collectors[i];
if (collectAmt + collector.amount > slashAmt) {
collector.amount = slashAmt - collectAmt;
}
if (collector.amount > 0) {
collectAmt += collector.amount;
if (collector.account == address(0)) {
CELER_TOKEN.safeTransfer(msg.sender, collector.amount);
emit SlashAmtCollected(msg.sender, collector.amount);
} else {
CELER_TOKEN.safeTransfer(collector.account, collector.amount);
emit SlashAmtCollected(collector.account, collector.amount);
}
}
}
forfeiture += slashAmt - collectAmt;
emit Slash(valAddr, request.nonce, slashAmt);
}
function collectForfeiture() external {
require(forfeiture > 0, "Nothing to collect");
CELER_TOKEN.safeTransfer(rewardContract, forfeiture);
forfeiture = 0;
}
/**
* @notice Validator notice event, could be triggered by anyone
*/
function validatorNotice(
address _valAddr,
string calldata _key,
bytes calldata _data
) external {
dt.Validator storage validator = validators[_valAddr];
require(validator.status != dt.ValidatorStatus.Null, "Validator is not initialized");
emit ValidatorNotice(_valAddr, _key, _data, msg.sender);
}
function setParamValue(dt.ParamName _name, uint256 _value) external {
require(msg.sender == govContract, "Caller is not gov contract");
if (_name == dt.ParamName.MaxBondedValidators) {
require(bondedValAddrs.length <= _value, "invalid value");
}
params[_name] = _value;
}
function setGovContract(address _addr) external onlyOwner {
govContract = _addr;
}
function setRewardContract(address _addr) external onlyOwner {
rewardContract = _addr;
}
/**
* @notice Set max slash factor
*/
function setMaxSlashFactor(uint256 _maxSlashFactor) external onlyOwner {
params[dt.ParamName.MaxSlashFactor] = _maxSlashFactor;
}
/**
* @notice Owner drains tokens when the contract is paused
* @dev emergency use only
* @param _amount drained token amount
*/
function drainToken(uint256 _amount) external whenPaused onlyOwner {
CELER_TOKEN.safeTransfer(msg.sender, _amount);
}
/**************************
* Public View Functions *
**************************/
/**
* @notice Validate if a message is signed by quorum tokens
* @param _msg signed message
* @param _sigs list of validator signatures
*/
function verifySignatures(bytes memory _msg, bytes[] memory _sigs) public view returns (bool) {
bytes32 hash = keccak256(_msg).toEthSignedMessageHash();
uint256 signedTokens;
address prev = address(0);
uint256 quorum = getQuorumTokens();
for (uint256 i = 0; i < _sigs.length; i++) {
address signer = hash.recover(_sigs[i]);
require(signer > prev, "Signers not in ascending order");
prev = signer;
dt.Validator storage validator = validators[signerVals[signer]];
if (validator.status != dt.ValidatorStatus.Bonded) {
continue;
}
signedTokens += validator.tokens;
if (signedTokens >= quorum) {
return true;
}
}
revert("Quorum not reached");
}
/**
* @notice Verifies that a message is signed by a quorum among the validators.
* @param _msg signed message
* @param _sigs the list of signatures
*/
function verifySigs(
bytes memory _msg,
bytes[] calldata _sigs,
address[] calldata,
uint256[] calldata
) public view override {
require(verifySignatures(_msg, _sigs), "Failed to verify sigs");
}
/**
* @notice Get quorum amount of tokens
* @return the quorum amount
*/
function getQuorumTokens() public view returns (uint256) {
return (bondedTokens * 2) / 3 + 1;
}
/**
* @notice Get validator info
* @param _valAddr the address of the validator
* @return Validator token amount
*/
function getValidatorTokens(address _valAddr) public view returns (uint256) {
return validators[_valAddr].tokens;
}
/**
* @notice Get validator info
* @param _valAddr the address of the validator
* @return Validator status
*/
function getValidatorStatus(address _valAddr) public view returns (dt.ValidatorStatus) {
return validators[_valAddr].status;
}
/**
* @notice Check the given address is a validator or not
* @param _addr the address to check
* @return the given address is a validator or not
*/
function isBondedValidator(address _addr) public view returns (bool) {
return validators[_addr].status == dt.ValidatorStatus.Bonded;
}
/**
* @notice Get the number of validators
* @return the number of validators
*/
function getValidatorNum() public view returns (uint256) {
return valAddrs.length;
}
/**
* @notice Get the number of bonded validators
* @return the number of bonded validators
*/
function getBondedValidatorNum() public view returns (uint256) {
return bondedValAddrs.length;
}
/**
* @return addresses and token amounts of bonded validators
*/
function getBondedValidatorsTokens() public view returns (dt.ValidatorTokens[] memory) {
dt.ValidatorTokens[] memory infos = new dt.ValidatorTokens[](bondedValAddrs.length);
for (uint256 i = 0; i < bondedValAddrs.length; i++) {
address valAddr = bondedValAddrs[i];
infos[i] = dt.ValidatorTokens(valAddr, validators[valAddr].tokens);
}
return infos;
}
/**
* @notice Check if min token requirements are met
* @param _valAddr the address of the validator
* @param _checkSelfDelegation check self delegation
*/
function hasMinRequiredTokens(address _valAddr, bool _checkSelfDelegation) public view returns (bool) {
dt.Validator storage v = validators[_valAddr];
uint256 valTokens = v.tokens;
if (valTokens < params[dt.ParamName.MinValidatorTokens]) {
return false;
}
if (_checkSelfDelegation) {
uint256 selfDelegation = _shareToToken(v.delegators[_valAddr].shares, valTokens, v.shares);
if (selfDelegation < v.minSelfDelegation) {
return false;
}
}
return true;
}
/**
* @notice Get the delegator info of a specific validator
* @param _valAddr the address of the validator
* @param _delAddr the address of the delegator
* @return DelegatorInfo from the given validator
*/
function getDelegatorInfo(address _valAddr, address _delAddr) public view returns (dt.DelegatorInfo memory) {
dt.Validator storage validator = validators[_valAddr];
dt.Delegator storage d = validator.delegators[_delAddr];
uint256 tokens = _shareToToken(d.shares, validator.tokens, validator.shares);
uint256 undelegationShares;
uint256 withdrawableUndelegationShares;
uint256 unbondingPeriod = params[dt.ParamName.UnbondingPeriod];
bool isUnbonded = validator.status == dt.ValidatorStatus.Unbonded;
uint256 len = d.undelegations.tail - d.undelegations.head;
dt.Undelegation[] memory undelegations = new dt.Undelegation[](len);
for (uint256 i = 0; i < len; i++) {
undelegations[i] = d.undelegations.queue[i + d.undelegations.head];
undelegationShares += undelegations[i].shares;
if (isUnbonded || undelegations[i].creationBlock + unbondingPeriod <= block.number) {
withdrawableUndelegationShares += undelegations[i].shares;
}
}
uint256 undelegationTokens = _shareToToken(
undelegationShares,
validator.undelegationTokens,
validator.undelegationShares
);
uint256 withdrawableUndelegationTokens = _shareToToken(
withdrawableUndelegationShares,
validator.undelegationTokens,
validator.undelegationShares
);
return
dt.DelegatorInfo(
_valAddr,
tokens,
d.shares,
undelegations,
undelegationTokens,
withdrawableUndelegationTokens
);
}
/**
* @notice Get the value of a specific uint parameter
* @param _name the key of this parameter
* @return the value of this parameter
*/
function getParamValue(dt.ParamName _name) public view returns (uint256) {
return params[_name];
}
/*********************
* Private Functions *
*********************/
function _undelegate(
dt.Validator storage validator,
address _valAddr,
uint256 _tokens,
uint256 _shares
) private {
address delAddr = msg.sender;
dt.Delegator storage delegator = validator.delegators[delAddr];
delegator.shares -= _shares;
validator.shares -= _shares;
validator.tokens -= _tokens;
if (validator.tokens != validator.shares && delegator.shares <= 2) {
// Remove residual share caused by rounding error when total shares and tokens are not equal
validator.shares -= delegator.shares;
delegator.shares = 0;
}
require(delegator.shares == 0 || delegator.shares >= dt.CELR_DECIMAL, "not enough remaining shares");
if (validator.status == dt.ValidatorStatus.Unbonded) {
CELER_TOKEN.safeTransfer(delAddr, _tokens);
emit Undelegated(_valAddr, delAddr, _tokens);
return;
} else if (validator.status == dt.ValidatorStatus.Bonded) {
bondedTokens -= _tokens;
if (!hasMinRequiredTokens(_valAddr, delAddr == _valAddr)) {
_unbondValidator(_valAddr);
}
}
require(
delegator.undelegations.tail - delegator.undelegations.head < dt.MAX_UNDELEGATION_ENTRIES,
"Exceed max undelegation entries"
);
uint256 undelegationShares = _tokenToShare(_tokens, validator.undelegationTokens, validator.undelegationShares);
validator.undelegationShares += undelegationShares;
validator.undelegationTokens += _tokens;
dt.Undelegation storage undelegation = delegator.undelegations.queue[delegator.undelegations.tail];
undelegation.shares = undelegationShares;
undelegation.creationBlock = block.number;
delegator.undelegations.tail++;
emit DelegationUpdate(_valAddr, delAddr, validator.tokens, delegator.shares, -int256(_tokens));
}
/**
* @notice Set validator to bonded
* @param _valAddr the address of the validator
*/
function _setBondedValidator(address _valAddr) private {
dt.Validator storage validator = validators[_valAddr];
validator.status = dt.ValidatorStatus.Bonded;
delete validator.unbondBlock;
bondedTokens += validator.tokens;
emit ValidatorStatusUpdate(_valAddr, dt.ValidatorStatus.Bonded);
}
/**
* @notice Set validator to unbonding
* @param _valAddr the address of the validator
*/
function _setUnbondingValidator(address _valAddr) private {
dt.Validator storage validator = validators[_valAddr];
validator.status = dt.ValidatorStatus.Unbonding;
validator.unbondBlock = uint64(block.number + params[dt.ParamName.UnbondingPeriod]);
bondedTokens -= validator.tokens;
emit ValidatorStatusUpdate(_valAddr, dt.ValidatorStatus.Unbonding);
}
/**
* @notice Bond a validator
* @param _valAddr the address of the validator
*/
function _bondValidator(address _valAddr) private {
bondedValAddrs.push(_valAddr);
_setBondedValidator(_valAddr);
}
/**
* @notice Replace a bonded validator
* @param _valAddr the address of the new validator
* @param _index the index of the validator to be replaced
*/
function _replaceBondedValidator(address _valAddr, uint256 _index) private {
_setUnbondingValidator(bondedValAddrs[_index]);
bondedValAddrs[_index] = _valAddr;
_setBondedValidator(_valAddr);
}
/**
* @notice Unbond a validator
* @param _valAddr validator to be removed
*/
function _unbondValidator(address _valAddr) private {
uint256 lastIndex = bondedValAddrs.length - 1;
for (uint256 i = 0; i < bondedValAddrs.length; i++) {
if (bondedValAddrs[i] == _valAddr) {
if (i < lastIndex) {
bondedValAddrs[i] = bondedValAddrs[lastIndex];
}
bondedValAddrs.pop();
_setUnbondingValidator(_valAddr);
return;
}
}
revert("Not bonded validator");
}
/**
* @notice Check if one validator has too much power
* @param _valTokens token amounts of the validator
*/
function _decentralizationCheck(uint256 _valTokens) private view {
uint256 bondedValNum = bondedValAddrs.length;
if (bondedValNum == 2 || bondedValNum == 3) {
require(_valTokens < getQuorumTokens(), "Single validator should not have quorum tokens");
} else if (bondedValNum > 3) {
require(_valTokens < bondedTokens / 3, "Single validator should not have 1/3 tokens");
}
}
/**
* @notice Convert token to share
*/
function _tokenToShare(
uint256 tokens,
uint256 totalTokens,
uint256 totalShares
) private pure returns (uint256) {
if (totalTokens == 0) {
return tokens;
}
return (tokens * totalShares) / totalTokens;
}
/**
* @notice Convert share to token
*/
function _shareToToken(
uint256 shares,
uint256 totalTokens,
uint256 totalShares
) private pure returns (uint256) {
if (totalShares == 0) {
return shares;
}
return (shares * totalTokens) / totalShares;
}
}
================================================
FILE: contracts/staking/StakingReward.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {DataTypes as dt} from "./DataTypes.sol";
import "../safeguard/Pauser.sol";
import "./Staking.sol";
/**
* @title A contract to hold and distribute CELR staking rewards.
*/
contract StakingReward is Pauser {
using SafeERC20 for IERC20;
Staking public immutable staking;
// recipient => CELR reward amount
mapping(address => uint256) public claimedRewardAmounts;
event StakingRewardClaimed(address indexed recipient, uint256 reward);
event StakingRewardContributed(address indexed contributor, uint256 contribution);
constructor(Staking _staking) {
staking = _staking;
}
/**
* @notice Claim reward
* @dev Here we use cumulative reward to make claim process idempotent
* @param _rewardRequest reward request bytes coded in protobuf
* @param _sigs list of validator signatures
*/
function claimReward(bytes calldata _rewardRequest, bytes[] calldata _sigs) external whenNotPaused {
bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "StakingReward"));
staking.verifySignatures(abi.encodePacked(domain, _rewardRequest), _sigs);
PbStaking.StakingReward memory reward = PbStaking.decStakingReward(_rewardRequest);
uint256 cumulativeRewardAmount = reward.cumulativeRewardAmount;
uint256 newReward = cumulativeRewardAmount - claimedRewardAmounts[reward.recipient];
require(newReward > 0, "No new reward");
claimedRewardAmounts[reward.recipient] = cumulativeRewardAmount;
staking.CELER_TOKEN().safeTransfer(reward.recipient, newReward);
emit StakingRewardClaimed(reward.recipient, newReward);
}
/**
* @notice Contribute CELR tokens to the reward pool
* @param _amount the amount of CELR token to contribute
*/
function contributeToRewardPool(uint256 _amount) external whenNotPaused {
address contributor = msg.sender;
IERC20(staking.CELER_TOKEN()).safeTransferFrom(contributor, address(this), _amount);
emit StakingRewardContributed(contributor, _amount);
}
/**
* @notice Owner drains CELR tokens when the contract is paused
* @dev emergency use only
* @param _amount drained CELR token amount
*/
function drainToken(uint256 _amount) external whenPaused onlyOwner {
IERC20(staking.CELER_TOKEN()).safeTransfer(msg.sender, _amount);
}
}
================================================
FILE: contracts/staking/Viewer.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import {DataTypes as dt} from "./DataTypes.sol";
import "./Staking.sol";
/**
* @title Viewer of the staking contract
* @notice Using a separate viewer contract to reduce staking contract size
*/
contract Viewer {
Staking public immutable staking;
constructor(Staking _staking) {
staking = _staking;
}
function getValidatorInfos() public view returns (dt.ValidatorInfo[] memory) {
uint256 valNum = staking.getValidatorNum();
dt.ValidatorInfo[] memory infos = new dt.ValidatorInfo[](valNum);
for (uint32 i = 0; i < valNum; i++) {
infos[i] = getValidatorInfo(staking.valAddrs(i));
}
return infos;
}
function getBondedValidatorInfos() public view returns (dt.ValidatorInfo[] memory) {
uint256 bondedValNum = staking.getBondedValidatorNum();
dt.ValidatorInfo[] memory infos = new dt.ValidatorInfo[](bondedValNum);
for (uint32 i = 0; i < bondedValNum; i++) {
infos[i] = getValidatorInfo(staking.bondedValAddrs(i));
}
return infos;
}
function getValidatorInfo(address _valAddr) public view returns (dt.ValidatorInfo memory) {
(
dt.ValidatorStatus status,
address signer,
uint256 tokens,
uint256 shares,
,
,
uint256 minSelfDelegation,
,
,
uint64 commissionRate
) = staking.validators(_valAddr);
return
dt.ValidatorInfo({
valAddr: _valAddr,
status: status,
signer: signer,
tokens: tokens,
shares: shares,
minSelfDelegation: minSelfDelegation,
commissionRate: commissionRate
});
}
function getDelegatorInfos(address _delAddr) public view returns (dt.DelegatorInfo[] memory) {
uint256 valNum = staking.getValidatorNum();
dt.DelegatorInfo[] memory infos = new dt.DelegatorInfo[](valNum);
uint32 num = 0;
for (uint32 i = 0; i < valNum; i++) {
address valAddr = staking.valAddrs(i);
infos[i] = staking.getDelegatorInfo(valAddr, _delAddr);
if (infos[i].shares != 0 || infos[i].undelegationTokens != 0) {
num++;
}
}
dt.DelegatorInfo[] memory res = new dt.DelegatorInfo[](num);
uint32 j = 0;
for (uint32 i = 0; i < valNum; i++) {
if (infos[i].shares != 0 || infos[i].undelegationTokens != 0) {
res[j] = infos[i];
j++;
}
}
return res;
}
function getDelegatorTokens(address _delAddr) public view returns (uint256, uint256) {
dt.DelegatorInfo[] memory infos = getDelegatorInfos(_delAddr);
uint256 tokens;
uint256 undelegationTokens;
for (uint32 i = 0; i < infos.length; i++) {
tokens += infos[i].tokens;
undelegationTokens += infos[i].undelegationTokens;
}
return (tokens, undelegationTokens);
}
/**
* @notice Get the minimum staking pool of all bonded validators
* @return the minimum staking pool of all bonded validators
*/
function getMinValidatorTokens() public view returns (uint256) {
uint256 bondedValNum = staking.getBondedValidatorNum();
if (bondedValNum < staking.params(dt.ParamName.MaxBondedValidators)) {
return 0;
}
uint256 minTokens = dt.MAX_INT;
for (uint256 i = 0; i < bondedValNum; i++) {
uint256 tokens = staking.getValidatorTokens(staking.bondedValAddrs(i));
if (tokens < minTokens) {
minTokens = tokens;
if (minTokens == 0) {
return 0;
}
}
}
return minTokens;
}
function shouldBondValidator(address _valAddr) public view returns (bool) {
(dt.ValidatorStatus status, , uint256 tokens, , , , , uint64 bondBlock, , ) = staking.validators(_valAddr);
if (status == dt.ValidatorStatus.Null || status == dt.ValidatorStatus.Bonded) {
return false;
}
if (block.number < bondBlock) {
return false;
}
if (!staking.hasMinRequiredTokens(_valAddr, true)) {
return false;
}
if (tokens <= getMinValidatorTokens()) {
return false;
}
uint256 nextBondBlock = staking.nextBondBlock();
if (block.number < nextBondBlock) {
return false;
}
return true;
}
}
================================================
FILE: contracts/test-helpers/DummySwap.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract DummySwap {
using SafeERC20 for IERC20;
uint256 fakeSlippage; // 100% = 100 * 1e4
uint256 hundredPercent = 100 * 1e4;
constructor(uint256 _fakeSlippage) {
fakeSlippage = _fakeSlippage;
}
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts) {
require(deadline != 0 && deadline > block.timestamp, "deadline exceeded");
require(path.length > 1, "path must have more than 1 token in it");
IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
// fake simulate slippage
uint256 amountAfterSlippage = (amountIn * (hundredPercent - fakeSlippage)) / hundredPercent;
require(amountAfterSlippage > amountOutMin, "bad slippage");
IERC20(path[path.length - 1]).safeTransfer(to, amountAfterSlippage);
uint256[] memory ret = new uint256[](2);
ret[0] = amountIn;
ret[1] = amountAfterSlippage;
return ret;
}
function setFakeSlippage(uint256 _fakeSlippage) public {
fakeSlippage = _fakeSlippage;
}
}
================================================
FILE: contracts/test-helpers/TestERC20.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title A test ERC20 token.
*/
contract TestERC20 is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1e28;
/**
* @dev Constructor that gives msg.sender all of the existing tokens.
*/
constructor() ERC20("TestERC20", "TERC20") {
_mint(msg.sender, INITIAL_SUPPLY);
}
}
================================================
FILE: contracts/test-helpers/WETH.sol
================================================
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract WETH is ERC20 {
constructor() ERC20("WETH", "WETH") {}
function deposit() external payable {
_mint(msg.sender, msg.value);
}
function withdraw(uint256 _amount) external {
_burn(msg.sender, _amount);
(bool sent, ) = msg.sender.call{value: _amount, gas: 50000}("");
require(sent, "failed to send");
}
receive() external payable {}
}
================================================
FILE: deploy/circle-usdc/000_circle_bridge_proxy.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('CircleBridgeProxy', {
from: deployer,
log: true,
args: [
process.env.CIRCLE_BRIDGE,
process.env.CIRCLE_BRIDGE_FEE_COLLECTOR
]
});
};
deployFunc.tags = ['CircleBridgeProxy'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/circle-usdc/000_circle_bridge_proxy_v2.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('CircleBridgeProxyV2', {
from: deployer,
log: true,
args: [
process.env.CIRCLE_BRIDGE,
process.env.CIRCLE_BRIDGE_FEE_COLLECTOR
]
});
};
deployFunc.tags = ['CircleBridgeProxyV2'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/core/000_sgn_staking.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('Staking', {
from: deployer,
log: true,
args: [
process.env.CELR,
process.env.PROPOSAL_DEPOSIT,
process.env.VOTING_PERIOD,
process.env.UNBONDING_PERIOD,
process.env.MAX_VALIDATOR_NUM,
process.env.MIN_VALIDATOR_TOKENS,
process.env.MIN_SELF_DELEGATION,
process.env.ADVANCE_NOTICE_PERIOD,
process.env.VALIDATOR_BOND_INTERVAL,
process.env.MAX_SLASH_FACTOR
]
});
const staking = await deployments.get('Staking');
await deploy('SGN', {
from: deployer,
log: true,
args: [staking.address]
});
await deploy('StakingReward', {
from: deployer,
log: true,
args: [staking.address]
});
const stakingReward = await deployments.get('StakingReward');
await deploy('Govern', {
from: deployer,
log: true,
args: [staking.address, process.env.CELR, stakingReward.address]
});
await deploy('Viewer', {
from: deployer,
log: true,
args: [staking.address]
});
};
deployFunc.tags = ['SGNStaking'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/governed-owner/000_governed_owner.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const ownerProxyArgs = [process.env.GOVERNANCE_INITIALIZER];
const governedOwnerProxy = await deploy('GovernedOwnerProxy', {
from: deployer,
log: true,
args: ownerProxyArgs
});
await hre.run('verify:verify', { address: governedOwnerProxy.address, constructorArguments: ownerProxyArgs });
const voters = (process.env.GOVERNANCE_VOTERS as string).split(',');
const powers = (process.env.GOVERNANCE_POWERS as string).split(',');
const governanceArgs = [
voters,
powers,
[governedOwnerProxy.address],
process.env.GOVERNANCE_ACTIVE_PERIOD,
process.env.GOVERNANCE_QUORUM_THRESHOLD,
process.env.GOVERNANCE_FAST_PASS_THRESHOLD
];
const simpleGovernance = await deploy('SimpleGovernance', {
from: deployer,
log: true,
args: governanceArgs
});
await hre.run('verify:verify', { address: simpleGovernance.address, constructorArguments: governanceArgs });
};
deployFunc.tags = ['GovernedOwner'];
export default deployFunc;
================================================
FILE: deploy/governed-owner/customized/000_message_bus_owner.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const voters = (process.env.GOVERNANCE_VOTERS as string).split(',');
const powers = (process.env.GOVERNANCE_POWERS as string).split(',');
const messageBusOwnerArgs = [
voters,
powers,
process.env.GOVERNANCE_ACTIVE_PERIOD,
process.env.GOVERNANCE_QUORUM_THRESHOLD
];
const messageBusOwner = await deploy('MessageBusOwner', {
from: deployer,
log: true,
args: messageBusOwnerArgs
});
await hre.run('verify:verify', { address: messageBusOwner.address, constructorArguments: messageBusOwnerArgs });
};
deployFunc.tags = ['MessageBusOwner'];
export default deployFunc;
================================================
FILE: deploy/liquidity-bridge/000_bridge.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const bridge = await deploy('Bridge', {
from: deployer,
log: true
});
await hre.run('verify:verify', { address: bridge.address });
};
deployFunc.tags = ['Bridge'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/liquidity-bridge/001_farming_rewards.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.FARMING_REWARDS_SIGS_VERIFIER];
const farmingRewards = await deploy('FarmingRewards', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: farmingRewards.address, constructorArguments: args });
};
deployFunc.tags = ['FarmingRewards'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/000_message_bus_init.ts
================================================
import * as dotenv from 'dotenv';
import { AbiCoder } from 'ethers';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { MessageBus__factory } from '../../typechain';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const initArgs: [string, string, string, string, string] = [
process.env.MESSAGE_BUS_LIQUIDITY_BRIDGE as string,
process.env.MESSAGE_BUS_PEG_BRIDGE as string,
process.env.MESSAGE_BUS_PEG_VAULT as string,
process.env.MESSAGE_BUS_PEG_BRIDGE_V2 as string,
process.env.MESSAGE_BUS_PEG_VAULT_V2 as string
];
const constructorArgs = [process.env.MESSAGE_BUS_SIGS_VERIFIER].concat(initArgs);
await deploy('MessageBus', {
from: deployer,
log: true,
args: constructorArgs,
proxy: {
proxyContract: 'OptimizedTransparentProxy',
execute: {
// only called when proxy is deployed, it'll call MessageBus contract.init
// with proper args
init: {
methodName: 'init',
args: initArgs
}
}
}
});
const messageBusInterface = MessageBus__factory.createInterface();
const encodedInitData = messageBusInterface.encodeFunctionData('init', initArgs);
console.log('Encoded init data', encodedInitData);
const proxyAdmin = await deployments.get('DefaultProxyAdmin');
console.log('DefaultProxyAdmin', proxyAdmin.address);
const proxy = await deployments.get('MessageBus_Proxy');
console.log('MessageBus_Proxy', proxy.address);
const messageBus = await deployments.get('MessageBus_Implementation');
await hre.run('verify:verify', { address: messageBus.address, constructorArguments: constructorArgs });
// Have to manually verify because hardhat-deploy compiles proxy with 0.8.10
// const proxyArgs = [messageBus.address, proxyAdmin.address].concat(encodedInitData);
// await hre.run('verify:verify', { address: proxy.address, constructorArguments: proxyArgs });
console.log(
'Encoded proxy constructor args',
AbiCoder.defaultAbiCoder().encode(
['address', 'address', 'bytes'],
[messageBus.address, proxyAdmin.address, encodedInitData]
)
);
};
deployFunc.tags = ['MessageBusInit'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/001_message_bus.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const constructorArgs = [
process.env.MESSAGE_BUS_SIGS_VERIFIER,
process.env.MESSAGE_BUS_LIQUIDITY_BRIDGE as string,
process.env.MESSAGE_BUS_PEG_BRIDGE as string,
process.env.MESSAGE_BUS_PEG_VAULT as string,
process.env.MESSAGE_BUS_PEG_BRIDGE_V2 as string,
process.env.MESSAGE_BUS_PEG_VAULT_V2 as string
];
const result = await deploy('MessageBus', {
from: deployer,
log: true,
args: constructorArgs
});
await hre.run('verify:verify', { address: result.address, constructorArguments: constructorArgs });
};
deployFunc.tags = ['MessageBus'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/000_transfer_swap.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('TransferSwap', {
from: deployer,
log: true,
args: [
process.env.CC_SWAP_MSG_BUS,
process.env.CC_SWAP_DEX,
process.env.CC_SWAP_BRIDGE,
process.env.CC_SWAP_BRIDGE_TOKEN,
process.env.CC_SWAP_NATIVE_WRAP
]
});
};
deployFunc.tags = ['TransferSwap'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/001_nft_bridge.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('NFTBridge', {
from: deployer,
log: true,
args: [
process.env.MSG_BUS_ADDR
],
proxy: {
proxyContract: "OptimizedTransparentProxy",
execute: {
// only called when proxy is deployed, it'll call NFTBridge.init
// to set owner and msgbus in proxy contract state
init: {
methodName: 'init',
args: [
process.env.MSG_BUS_ADDR
]
}
}
}
});
};
deployFunc.tags = ['NFTBridge'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/002_peg_nft.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('PegNFT', {
from: deployer,
log: true,
args: [
process.env.NFT_NAME,
process.env.NFT_SYM,
process.env.NFT_BRIDGE_ADDR
]
});
};
deployFunc.tags = ['PegNFT'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/003_orig_nft.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('OrigNFT', {
from: deployer,
log: true,
args: [
process.env.NFT_NAME,
process.env.NFT_SYM
]
});
};
deployFunc.tags = ['OrigNFT'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/004_mcn_nft.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MCNNFT', {
from: deployer,
log: true,
args: [
process.env.NFT_NAME,
process.env.NFT_SYM,
process.env.NFT_BRIDGE_ADDR
]
});
};
deployFunc.tags = ['MCNNFT'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/005_msg_test.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MsgTest', {
from: deployer,
log: true,
args: [
process.env.MESSAGE_BUS_ADDR,
]
});
};
deployFunc.tags = ['MsgTest'];
export default deployFunc;
================================================
FILE: deploy/message/apps/006_rfq.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('RFQ', {
from: deployer,
log: true,
args: [process.env.MESSAGE_BUS_ADDR]
});
};
deployFunc.tags = ['RFQ'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/message/apps/007_adapter.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MessageReceiverAdapter', {
from: deployer,
log: true,
args: [process.env.MESSAGE_BUS_ADDR]
});
};
deployFunc.tags = ['MessageReceiverAdapter'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/miscs/000_test_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.TEST_TOKEN_NAME,
process.env.TEST_TOKEN_SYMBOL,
process.env.TEST_TOKEN_DECIMALS,
process.env.TEST_TOKEN_SUPPLY
];
const testToken = await deploy('MintableERC20', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: testToken.address, constructorArguments: args });
};
deployFunc.tags = ['Test' + (process.env.TEST_TOKEN_SYMBOL || 'Token')];
export default deployFunc;
================================================
FILE: deploy/miscs/001_faucet.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('Faucet', {
from: deployer,
log: true
});
};
deployFunc.tags = ['Faucet'];
export default deployFunc;
================================================
FILE: deploy/miscs/002_dummy_swap.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('DummySwap', {
from: deployer,
log: true,
args: [50000] // 5% fake slippage
});
};
deployFunc.tags = ['DummySwap'];
export default deployFunc;
================================================
FILE: deploy/miscs/003_withdraw_inbox.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const withdrawInbox = await deploy('WithdrawInbox', {
from: deployer,
log: true
});
await hre.run('verify:verify', { address: withdrawInbox.address });
};
deployFunc.tags = ['WithdrawInbox'];
export default deployFunc;
================================================
FILE: deploy/miscs/004_contract_as_lp.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('ContractAsLP', {
from: deployer,
log: true,
args: [process.env.BRIDGE, process.env.WITHDRAW_INBOX]
});
};
deployFunc.tags = ['ContractAsLP'];
export default deployFunc;
================================================
FILE: deploy/miscs/005_contract_as_sender.ts
================================================
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('ContractAsSender', {
from: deployer,
log: true
});
};
deployFunc.tags = ['ContractAsSender'];
export default deployFunc;
================================================
FILE: deploy/miscs/006_transfer_agent.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('TransferAgent', {
from: deployer,
log: true
});
};
deployFunc.tags = ['TransferAgent'];
export default deployFunc;
================================================
FILE: deploy/miscs/007_default_proxy_admin.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.PROXY_ADMIN_OWNER];
const proxyAdmin = await deploy('DefaultProxyAdmin', {
contract: 'ProxyAdmin',
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: proxyAdmin.address, constructorArguments: args });
};
deployFunc.tags = ['DefaultProxyAdmin'];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/000_original_token_vault.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('OriginalTokenVault', {
from: deployer,
log: true,
args: [process.env.ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER]
});
};
deployFunc.tags = ['OriginalTokenVault'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/001_pegged_token_bridge.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('PeggedTokenBridge', {
from: deployer,
log: true,
args: [process.env.PEGGED_TOKEN_BRIDGE_SIGS_VERIFIER]
});
};
deployFunc.tags = ['PeggedTokenBridge'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/002_original_token_vault_v2.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.ORIGINAL_TOKEN_VAULT_SIGS_VERIFIER];
const originalTokenVaultV2 = await deploy('OriginalTokenVaultV2', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: originalTokenVaultV2.address, constructorArguments: args });
};
deployFunc.tags = ['OriginalTokenVaultV2'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/003_pegged_token_bridge_v2.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.PEGGED_TOKEN_BRIDGE_SIGS_VERIFIER];
const peggedTokenBridgeV2 = await deploy('PeggedTokenBridgeV2', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: peggedTokenBridgeV2.address, constructorArguments: args });
};
deployFunc.tags = ['PeggedTokenBridgeV2'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/004_pegged_brc20_bridge.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.PEGGED_BRC20_BRIDGE_MINTER];
const peggedBrc20Bridge = await deploy('PeggedBrc20Bridge', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: peggedBrc20Bridge.address, constructorArguments: args });
};
deployFunc.tags = ['PeggedBrc20Bridge'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/customized/000_pegged_native_token_bridge.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [process.env.PEGGED_TOKEN_BRIDGE_SIGS_VERIFIER];
const peggedTokenBridgeV2 = await deploy('PeggedNativeTokenBridge', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: peggedTokenBridgeV2.address, constructorArguments: args });
};
deployFunc.tags = ['PeggedNativeTokenBridge'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/000_single_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('SingleBridgeToken', {
from: deployer,
log: true,
args: [
process.env.SINGLE_BRIDGE_TOKEN_NAME,
process.env.SINGLE_BRIDGE_TOKEN_SYMBOL,
process.env.SINGLE_BRIDGE_TOKEN_DECIMALS,
process.env.SINGLE_BRIDGE_TOKEN_BRIDGE
]
});
};
deployFunc.tags = ['SingleBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/001_single_bridge_token_permit.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('SingleBridgeTokenPermit', {
from: deployer,
log: true,
args: [
process.env.SINGLE_BRIDGE_TOKEN_PERMIT_NAME,
process.env.SINGLE_BRIDGE_TOKEN_PERMIT_SYMBOL,
process.env.SINGLE_BRIDGE_TOKEN_PERMIT_DECIMALS,
process.env.SINGLE_BRIDGE_TOKEN_PERMIT_BRIDGE
]
});
};
deployFunc.tags = ['SingleBridgeTokenPermit'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/002_multi_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.MULTI_BRIDGE_TOKEN_NAME,
process.env.MULTI_BRIDGE_TOKEN_SYMBOL,
process.env.MULTI_BRIDGE_TOKEN_DECIMALS
];
const multiBridgeToken = await deploy('MultiBridgeToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: multiBridgeToken.address, constructorArguments: args });
};
deployFunc.tags = ['MultiBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/003_multi_bridge_token_permit.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MultiBridgeTokenPermit', {
from: deployer,
log: true,
args: [
process.env.MULTI_BRIDGE_TOKEN_PERMIT_NAME,
process.env.MULTI_BRIDGE_TOKEN_PERMIT_SYMBOL,
process.env.MULTI_BRIDGE_TOKEN_PERMIT_DECIMALS
]
});
};
deployFunc.tags = ['MultiBridgeTokenPermit'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/004_mint_swap_canonical_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL,
process.env.MINT_SWAP_CANONICAL_TOKEN_DECIMALS
];
const mintSwapCanonicalToken = await deploy('MintSwapCanonicalToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: mintSwapCanonicalToken.address, constructorArguments: args });
};
deployFunc.tags = ['MintSwapCanonicalToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/005_mint_swap_canonical_token_permit.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MintSwapCanonicalTokenPermit', {
from: deployer,
log: true,
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_PERMIT_NAME,
process.env.MULTI_BRIDGE_TOKEN_PERMIT_SYMBOL,
process.env.MULTI_BRIDGE_TOKEN_PERMIT_DECIMALS
]
});
};
deployFunc.tags = ['MintSwapCanonicalTokenPermit'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/006_mint_swap_canonical_token_upgradable.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MintSwapCanonicalTokenUpgradable', {
from: deployer,
log: true,
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL,
process.env.MINT_SWAP_CANONICAL_TOKEN_DECIMALS
],
proxy: {
proxyContract: "OptimizedTransparentProxy",
execute: {
// only called when proxy is deployed, it'll call Token contract.init
// with proper args
init: {
methodName: 'init',
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL]
}
}
}
});
};
deployFunc.tags = ['MintSwapCanonicalTokenUpgradable'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/007_wrapped_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.WRAPPED_BRIDGE_TOKEN_NAME,
process.env.WRAPPED_BRIDGE_TOKEN_SYMBOL,
process.env.WRAPPED_BRIDGE_TOKEN_BRIDGE,
process.env.WRAPPED_BRIDGE_TOKEN_CANONICAL
];
const wrappedBridgeToken = await deploy('WrappedBridgeToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: wrappedBridgeToken.address, constructorArguments: args });
};
deployFunc.tags = ['WrappedBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/008_intermediary_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.INTERMEDIARY_BRIDGE_TOKEN_NAME,
process.env.INTERMEDIARY_BRIDGE_TOKEN_SYMBOL,
process.env.INTERMEDIARY_BRIDGE_TOKEN_BRIDGE,
process.env.INTERMEDIARY_BRIDGE_TOKEN_CANONICAL
];
const intermediaryBridgeToken = await deploy('IntermediaryBridgeToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: intermediaryBridgeToken.address, constructorArguments: args });
};
deployFunc.tags = ['IntermediaryBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/009_intermediary_original_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const bridges = (process.env.INTERMEDIARY_ORIGINAL_TOKEN_BRIDGES as string).split(',');
const args = [
process.env.INTERMEDIARY_ORIGINAL_TOKEN_NAME,
process.env.INTERMEDIARY_ORIGINAL_TOKEN_SYMBOL,
bridges,
process.env.INTERMEDIARY_ORIGINAL_TOKEN_CANONICAL
];
const intermediaryOriginalToken = await deploy('IntermediaryOriginalToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: intermediaryOriginalToken.address, constructorArguments: args });
};
deployFunc.tags = ['IntermediaryOriginalToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/010_circle_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.CIRCLE_BRIDGE_TOKEN_NAME,
process.env.CIRCLE_BRIDGE_TOKEN_SYMBOL,
process.env.CIRCLE_BRIDGE_TOKEN_BRIDGE,
process.env.CIRCLE_BRIDGE_TOKEN_CANONICAL,
process.env.CIRCLE_BRIDGE_TOKEN_ORIG_CHAIN_ID
];
const circleBridgeToken = await deploy('CircleBridgeToken', {
from: deployer,
log: true,
args: args
});
const txHash = circleBridgeToken.transactionHash!;
const tx = await hre.ethers.provider.getTransaction(txHash);
console.log('deploy tx data');
console.log(tx.data);
await hre.run('verify:verify', { address: circleBridgeToken.address, constructorArguments: args });
};
deployFunc.tags = ['CircleBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/customized/000_frax_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args = [
process.env.FRAX_BRIDGE_TOKEN_NAME,
process.env.FRAX_BRIDGE_TOKEN_SYMBOL,
process.env.FRAX_BRIDGE_TOKEN_BRIDGE,
process.env.FRAX_BRIDGE_TOKEN_CANONICAL
];
const fraxBridgeToken = await deploy('FraxBridgeToken', {
from: deployer,
log: true,
args: args
});
await hre.run('verify:verify', { address: fraxBridgeToken.address, constructorArguments: args });
};
deployFunc.tags = ['FraxBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/customized/001_mai_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MaiBridgeToken', {
from: deployer,
log: true,
args: [
process.env.MAI_BRIDGE_TOKEN_NAME,
process.env.MAI_BRIDGE_TOKEN_SYMBOL,
process.env.MAI_BRIDGE_TOKEN_BRIDGE,
process.env.MAI_BRIDGE_TOKEN_HUB
]
});
};
deployFunc.tags = ['MaiBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/customized/002_ontology_bridge_token.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('OntologyBridgeToken', {
from: deployer,
log: true,
args: [
process.env.ONTOLOGY_BRIDGE_TOKEN_NAME,
process.env.ONTOLOGY_BRIDGE_TOKEN_SYMBOL,
process.env.ONTOLOGY_BRIDGE_TOKEN_BRIDGE,
process.env.ONTOLOGY_BRIDGE_TOKEN_WRAPPER,
process.env.ONTOLOGY_BRIDGE_TOKEN_CANONICAL
]
});
};
deployFunc.tags = ['OntologyBridgeToken'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/freezable/000_mint_swap_canonical_token_upgradable_freezable.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MintSwapCanonicalTokenUpgradableFreezable', {
from: deployer,
log: true,
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL,
process.env.MINT_SWAP_CANONICAL_TOKEN_DECIMALS
],
proxy: {
proxyContract: 'OptimizedTransparentProxy',
execute: {
// only called when proxy is deployed, it'll call Token contract.init
// with proper args
init: {
methodName: 'init',
args: [process.env.MINT_SWAP_CANONICAL_TOKEN_NAME, process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL]
}
}
}
});
};
deployFunc.tags = ['MintSwapCanonicalTokenUpgradableFreezable'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/freezable/001_mint_swap_canonical_token_freezable.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('MintSwapCanonicalTokenFreezable', {
from: deployer,
log: true,
args: [
process.env.MINT_SWAP_CANONICAL_TOKEN_NAME,
process.env.MINT_SWAP_CANONICAL_TOKEN_SYMBOL,
process.env.MINT_SWAP_CANONICAL_TOKEN_DECIMALS
]
});
};
deployFunc.tags = ['MintSwapCanonicalTokenFreezable'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/pegged-bridge/tokens/owners/000_restricted_multi_bridge_token_owner.ts
================================================
import * as dotenv from 'dotenv';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy('RestrictedMultiBridgeTokenOwner', {
from: deployer,
log: true,
args: [
process.env.RESTRICTED_MULTI_BRIDGE_TOKEN_OWNER_TOKEN,
process.env.RESTRICTED_MULTI_BRIDGE_TOKEN_OWNER_BRIDGE
]
});
};
deployFunc.tags = ['RestrictedMultiBridgeTokenOwner'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/sentinel/000_sentinel.ts
================================================
import * as dotenv from 'dotenv';
import { AbiCoder } from 'ethers';
import { DeployFunction } from 'hardhat-deploy/types';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { Sentinel__factory } from '../../typechain';
dotenv.config();
const deployFunc: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
const args: [string[], string[], string[]] = [
(process.env.SENTINEL_GUARDS as string).split(','),
(process.env.SENTINEL_PAUSERS as string).split(','),
(process.env.SENTINEL_GOVERNORS as string).split(',')
];
await deploy('Sentinel', {
from: deployer,
log: true,
args: args,
proxy: {
proxyContract: 'OptimizedTransparentProxy',
owner: process.env.PROXY_ADMIN_OWNER,
viaAdminContract: { name: 'DefaultProxyAdmin', artifact: 'ProxyAdmin' }, // TODO: Check
execute: {
init: {
methodName: 'init',
args: args
}
}
}
});
const sentinelInterface = Sentinel__factory.createInterface();
const encodedInitData = sentinelInterface.encodeFunctionData('init', args);
console.log('Encoded init data', encodedInitData);
const proxyAdmin = await deployments.get('DefaultProxyAdmin');
console.log('DefaultProxyAdmin', proxyAdmin.address);
const proxy = await deployments.get('Sentinel_Proxy');
console.log('Sentinel_Proxy', proxy.address);
const sentinel = await deployments.get('Sentinel_Implementation');
await hre.run('verify:verify', { address: sentinel.address, constructorArguments: args });
// Have to manually verify because hardhat-deploy compiles proxy with 0.8.10
// const proxyArgs = [sentinel.address, proxyAdmin.address].concat(encodedInitData);
// await hre.run('verify:verify', { address: proxy.address, constructorArguments: proxyArgs });
console.log(
'Encoded proxy constructor args',
AbiCoder.defaultAbiCoder().encode(
['address', 'address', 'bytes'],
[sentinel.address, proxyAdmin.address, encodedInitData]
)
);
};
deployFunc.tags = ['SentinelUpgradable'];
deployFunc.dependencies = [];
export default deployFunc;
================================================
FILE: deploy/sentinel/001_sentinel_zksync.ts
================================================
// Uncomment for zksync
// import * as dotenv from 'dotenv';
// import * as hre from 'hardhat';
// import { Wallet } from 'zksync-web3';
// import { Deployer } from '@matterlabs/hardhat-zksync-deploy';
// dotenv.config();
// async function deploy() {
// const contractName = 'Sentinel';
// console.log('Deploying ' + contractName + '...');
// const zkWallet = new Wallet(process.env.ZKSYNC_ERA_PRIVATE_KEY as string);
// const deployer = new Deployer(hre, zkWallet);
// const contract = await deployer.loadArtifact(contractName);
// const args: [string[], string[], string[]] = [
// (process.env.SENTINEL_GUARDS as string).split(','),
// (process.env.SENTINEL_PAUSERS as string).split(','),
// (process.env.SENTINEL_GOVERNORS as string).split(',')
// ];
// const sentinelProxy = await hre.zkUpgrades.deployProxy(deployer.zkWallet, contract, args, {
// initializer: 'init',
// unsafeAllow: ['constructor']
// });
// await sentinelProxy.deployed();
// console.log(contractName + ' deployed to:', sentinelProxy.address);
// }
// deploy().catch((error) => {
// console.error(error);
// process.exitCode = 1;
// });
================================================
FILE: hardhat.config.ts
================================================
import '@nomicfoundation/hardhat-ethers';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-verify';
import '@typechain/hardhat';
import 'hardhat-contract-sizer';
import 'hardhat-deploy';
import 'hardhat-gas-reporter';
import 'hardhat-signer-kms';
import '@oasisprotocol/sapphire-hardhat';
import '@matterlabs/hardhat-zksync-deploy';
import '@matterlabs/hardhat-zksync-solc';
// Imports the verify plugin before the upgradable plugin
import '@matterlabs/hardhat-zksync-verify';
import '@matterlabs/hardhat-zksync-upgradable';
import * as dotenv from 'dotenv';
import { HardhatUserConfig, HttpNetworkUserConfig, NetworkUserConfig } from 'hardhat/types';
dotenv.config();
const DEFAULT_ENDPOINT = 'http://localhost:8545';
const DEFAULT_PRIVATE_KEY =
process.env.DEFAULT_PRIVATE_KEY || 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
const kmsKeyId = process.env.KMS_KEY_ID || '';
// Testnets
const kovanEndpoint = process.env.KOVAN_ENDPOINT || DEFAULT_ENDPOINT;
const kovanPrivateKey = process.env.KOVAN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const ropstenEndpoint = process.env.ROPSTEN_ENDPOINT || DEFAULT_ENDPOINT;
const ropstenPrivateKey = process.env.ROPSTEN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const goerliEndpoint = process.env.GOERLI_ENDPOINT || DEFAULT_ENDPOINT;
const goerliPrivateKey = process.env.GOERLI_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const bscTestEndpoint = process.env.BSC_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const bscTestPrivateKey = process.env.BSC_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const optimismTestEndpoint = process.env.OPTIMISM_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const optimismTestPrivateKey = process.env.OPTIMISM_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const fantomTestEndpoint = process.env.FANTOM_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const fantomTestPrivateKey = process.env.FANTOM_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const avalancheTestEndpoint = process.env.AVALANCHE_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const avalancheTestPrivateKey = process.env.AVALANCHE_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const celoAlfajoresTestEndpoint = process.env.CELO_ALFAJORES_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const celoAlfajoresTestPrivateKey = process.env.CELO_ALFAJORES_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasisEmeraldTestEndpoint = process.env.OASIS_EMERALD_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const oasisEmeraldTestPrivateKey = process.env.OASIS_EMERALD_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasisSapphireTestEndpoint = process.env.OASIS_SAPPHIRE_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const oasisSapphireTestPrivateKey = process.env.OASIS_SAPPHIRE_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const moonbaseAlphaTestEndpoint = process.env.MOONBASE_ALPHA_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const moonbaseAlphaTestPrivateKey = process.env.MOONBASE_ALPHA_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const reiTestEndpoint = process.env.REI_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const reiTestPrivateKey = process.env.REI_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const nervosGodwokenTestEndpoint = process.env.NERVOS_GODWOKEN_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const nervosGodwokenTestPrivateKey = process.env.NERVOS_GODWOKEN_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const kavaTestEndpoint = process.env.KAVA_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const kavaTestPrivateKey = process.env.KAVA_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const darwiniaPangolinTestEndpoint = process.env.DARWINIA_PANGOLIN_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const darwiniaPangolinTestPrivateKey = process.env.DARWINIA_PANGOLIN_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const platonTestEndpoint = process.env.PLATON_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const platonTestPrivateKey = process.env.PLATON_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const polygonTestEndpoint = process.env.POLYGON_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const polygonTestPrivateKey = process.env.POLYGON_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const sxTestEndpoint = process.env.SX_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const sxTestPrivateKey = process.env.SX_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const swimmerTestEndpoint = process.env.SWIMMER_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const swimmerTestPrivateKey = process.env.SWIMMER_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const dexalotTestEndpoint = process.env.DEXALOT_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const dexalotTestPrivateKey = process.env.DEXALOT_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const nervosTestnetEndpoint = process.env.NERVOS_TESTNET_ENDPOINT || DEFAULT_ENDPOINT;
const nervosTestnetPrivateKey = process.env.NERVOS_TESTNET_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const shibuyaTestnetEndpoint = process.env.SHIBUYA_TESTNET_ENDPOINT || DEFAULT_ENDPOINT;
const shibuyaTestnetPrivateKey = process.env.SHIBUYA_TESTNET_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const cubeDevnetEndpoint = process.env.CUBE_DEVNET_ENDPOINT || DEFAULT_ENDPOINT;
const cubeDevnetPrivateKey = process.env.CUBE_DEVNET_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasysTestEndpoint = process.env.OASYS_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const oasysTestPrivateKey = process.env.OASYS_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const antimatterB2TestEndpoint = process.env.ANTIMATTER_B2_TEST_ENDPOINT || DEFAULT_ENDPOINT;
const antimatterB2TestPrivateKey = process.env.ANTIMATTER_B2_TEST_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const holeskyEndpoint = process.env.HOLESKY_ENDPOINT || DEFAULT_ENDPOINT;
const holeskyPrivateKey = process.env.HOLESKY_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
// Mainnets
const ethMainnetEndpoint = process.env.ETH_MAINNET_ENDPOINT || DEFAULT_ENDPOINT;
const ethMainnetPrivateKey = process.env.ETH_MAINNET_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const bscEndpoint = process.env.BSC_ENDPOINT || DEFAULT_ENDPOINT;
const bscPrivateKey = process.env.BSC_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const arbitrumOneEndpoint = process.env.ARBITRUM_ONE_ENDPOINT || DEFAULT_ENDPOINT;
const arbitrumOnePrivateKey = process.env.ARBITRUM_ONE_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const arbitrumNovaEndpoint = process.env.ARBITRUM_NOVA_ENDPOINT || DEFAULT_ENDPOINT;
const arbitrumNovaPrivateKey = process.env.ARBITRUM_NOVA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const polygonEndpoint = process.env.POLYGON_ENDPOINT || DEFAULT_ENDPOINT;
const polygonPrivateKey = process.env.POLYGON_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const fantomEndpoint = process.env.FANTOM_ENDPOINT || DEFAULT_ENDPOINT;
const fantomPrivateKey = process.env.FANTOM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const avalancheEndpoint = process.env.AVALANCHE_ENDPOINT || DEFAULT_ENDPOINT;
const avalanchePrivateKey = process.env.AVALANCHE_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const optimismEndpoint = process.env.OPTIMISM_ENDPOINT || DEFAULT_ENDPOINT;
const optimismPrivateKey = process.env.OPTIMISM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const bobaEndpoint = process.env.BOBA_ENDPOINT || DEFAULT_ENDPOINT;
const bobaPrivateKey = process.env.BOBA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const harmonyEndpoint = process.env.HARMONY_ENDPOINT || DEFAULT_ENDPOINT;
const harmonyPrivateKey = process.env.HARMONY_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const moonbeamEndpoint = process.env.MOONBEAM_ENDPOINT || DEFAULT_ENDPOINT;
const moonbeamPrivateKey = process.env.MOONBEAM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const moonriverEndpoint = process.env.MOONRIVER_ENDPOINT || DEFAULT_ENDPOINT;
const moonriverPrivateKey = process.env.MOONRIVER_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const celoEndpoint = process.env.CELO_ENDPOINT || DEFAULT_ENDPOINT;
const celoPrivateKey = process.env.CELO_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasisEmeraldEndpoint = process.env.OASIS_EMERALD_ENDPOINT || DEFAULT_ENDPOINT;
const oasisEmeraldPrivateKey = process.env.OASIS_EMERALD_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasisSapphireEndpoint = process.env.OASIS_SAPPHIRE_ENDPOINT || DEFAULT_ENDPOINT;
const oasisSapphirePrivateKey = process.env.OASIS_SAPPHIRE_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const metisEndpoint = process.env.METIS_ENDPOINT || DEFAULT_ENDPOINT;
const metisPrivateKey = process.env.METIS_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const auroraEndpoint = process.env.AURORA_ENDPOINT || DEFAULT_ENDPOINT;
const auroraPrivateKey = process.env.AURORA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const xdaiEndpoint = process.env.XDAI_ENDPOINT || DEFAULT_ENDPOINT;
const xdaiPrivateKey = process.env.XDAI_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oecEndpoint = process.env.OEC_ENDPOINT || DEFAULT_ENDPOINT;
const oecPrivateKey = process.env.OEC_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const hecoEndpoint = process.env.HECO_ENDPOINT || DEFAULT_ENDPOINT;
const hecoPrivateKey = process.env.HECO_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const astarEndpoint = process.env.ASTAR_ENDPOINT || DEFAULT_ENDPOINT;
const astarPrivateKey = process.env.ASTAR_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const shidenEndpoint = process.env.SHIDEN_ENDPOINT || DEFAULT_ENDPOINT;
const shidenPrivateKey = process.env.SHIDEN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const syscoinEndpoint = process.env.SYSCOIN_ENDPOINT || DEFAULT_ENDPOINT;
const syscoinPrivateKey = process.env.SYSCOIN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const milkomedaC1Endpoint = process.env.MILKOMEDA_C1_ENDPOINT || DEFAULT_ENDPOINT;
const milkomedaC1PrivateKey = process.env.MILKOMEDA_C1_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const milkomedaA1Endpoint = process.env.MILKOMEDA_A1_ENDPOINT || DEFAULT_ENDPOINT;
const milkomedaA1PrivateKey = process.env.MILKOMEDA_A1_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const evmosEndpoint = process.env.EVMOS_ENDPOINT || DEFAULT_ENDPOINT;
const evmosPrivateKey = process.env.EVMOS_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const cloverEndpoint = process.env.CLOVER_ENDPOINT || DEFAULT_ENDPOINT;
const cloverPrivateKey = process.env.CLOVER_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const reiEndpoint = process.env.REI_ENDPOINT || DEFAULT_ENDPOINT;
const reiPrivateKey = process.env.REI_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const confluxEndpoint = process.env.CONFLUX_ENDPOINT || DEFAULT_ENDPOINT;
const confluxPrivateKey = process.env.CONFLUX_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const darwiniaCrabEndpoint = process.env.DARWINIA_CRAB_ENDPOINT || DEFAULT_ENDPOINT;
const darwiniaCrabPrivateKey = process.env.DARWINIA_CRAB_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const platonEndpoint = process.env.PLATON_ENDPOINT || DEFAULT_ENDPOINT;
const platonPrivateKey = process.env.PLATON_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const ontologyEndpoint = process.env.ONTOLOGY_ENDPOINT || DEFAULT_ENDPOINT;
const ontologyPrivateKey = process.env.ONTOLOGY_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const swimmerEndpoint = process.env.SWIMMER_ENDPOINT || DEFAULT_ENDPOINT;
const swimmerPrivateKey = process.env.SWIMMER_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const sxNetworkEndpoint = process.env.SX_NETWORK_ENDPOINT || DEFAULT_ENDPOINT;
const sxNetworkPrivateKey = process.env.SX_NETWORK_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const apeEndpoint = process.env.APE_ENDPOINT || DEFAULT_ENDPOINT;
const apePrivateKey = process.env.APE_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const kavaEndpoint = process.env.KAVA_ENDPOINT || DEFAULT_ENDPOINT;
const kavaPrivateKey = process.env.KAVA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const fncyEndpoint = process.env.FNCY_ENDPOINT || DEFAULT_ENDPOINT;
const fncyPrivateKey = process.env.FNCY_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const nervosGodwokenEndpoint = process.env.NERVOS_GODWOKEN_ENDPOINT || DEFAULT_ENDPOINT;
const nervosGodwokenPrivateKey = process.env.NERVOS_GODWOKEN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const klaytnEndpoint = process.env.KLAYTN_ENDPOINT || DEFAULT_ENDPOINT;
const klaytnPrivateKey = process.env.KLAYTN_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const oasysEndpoint = process.env.OASYS_ENDPOINT || DEFAULT_ENDPOINT;
const oasysPrivateKey = process.env.OASYS_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const spsEndpoint = process.env.SPS_ENDPOINT || DEFAULT_ENDPOINT;
const spsPrivateKey = process.env.SPS_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const fvmEndpoint = process.env.FVM_ENDPOINT || DEFAULT_ENDPOINT;
const fvmPrivateKey = process.env.FVM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const cantoEndpoint = process.env.CANTO_ENDPOINT || DEFAULT_ENDPOINT;
const cantoPrivateKey = process.env.CANTO_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const polygonZkevmEndpoint = process.env.POLYGON_ZKEVM_ENDPOINT || DEFAULT_ENDPOINT;
const polygonZkevmPrivateKey = process.env.POLYGON_ZKEVM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const antimatterB2Endpoint = process.env.ANTIMATTER_B2_ENDPOINT || DEFAULT_ENDPOINT;
const antimatterB2PrivateKey = process.env.ANTIMATTER_B2_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const lineaEndpoint = process.env.LINEA_ENDPOINT || DEFAULT_ENDPOINT;
const lineaPrivateKey = process.env.LINEA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const baseEndpoint = process.env.BASE_ENDPOINT || DEFAULT_ENDPOINT;
const basePrivateKey = process.env.BASE_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const telosEndpoint = process.env.TELOS_ENDPOINT || DEFAULT_ENDPOINT;
const telosPrivateKey = process.env.TELOS_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const scrollEndpoint = process.env.SCROLL_ENDPOINT || DEFAULT_ENDPOINT;
const scrollPrivateKey = process.env.SCROLL_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const zksyncEraEndpoint = process.env.ZKSYNC_ERA_ENDPOINT || DEFAULT_ENDPOINT;
const zksyncEraPrivateKey = process.env.ZKSYNC_ERA_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
const flowEvmEndpoint = process.env.FLOW_EVM_ENDPOINT || DEFAULT_ENDPOINT;
const flowEvmPrivateKey = process.env.FLOW_EVM_PRIVATE_KEY || DEFAULT_PRIVATE_KEY;
// use kmsKeyId if it's not empty, otherwise use privateKey
function getNetworkConfig(url: string, kmsKeyId: string, privateKey: string, gasPrice?: number): NetworkUserConfig {
const network: NetworkUserConfig = !kmsKeyId
? {
url: url,
accounts: [`0x${privateKey}`]
}
: {
url: url,
kmsKeyId: kmsKeyId
};
if (gasPrice) {
network.gasPrice = gasPrice;
}
return network;
}
const zksyncEraNetwork = getNetworkConfig(zksyncEraEndpoint, kmsKeyId, zksyncEraPrivateKey);
zksyncEraNetwork.zksync = true;
(zksyncEraNetwork as HttpNetworkUserConfig).ethNetwork = 'ethMainnet';
zksyncEraNetwork.verifyURL = 'https://zksync2-mainnet-explorer.zksync.io/contract_verification';
const config: HardhatUserConfig = {
defaultNetwork: 'hardhat',
networks: {
// Testnets
hardhat: {},
localhost: { timeout: 600000 },
kovan: {
url: kovanEndpoint,
accounts: [`0x${kovanPrivateKey}`],
ethNetwork: ''
},
ropsten: {
url: ropstenEndpoint,
accounts: [`0x${ropstenPrivateKey}`]
},
holesky: {
url: holeskyEndpoint,
accounts: [`0x${holeskyPrivateKey}`]
},
goerli: {
url: goerliEndpoint,
accounts: [`0x${goerliPrivateKey}`]
},
bscTest: {
url: bscTestEndpoint,
accounts: [`0x${bscTestPrivateKey}`]
},
fantomTest: {
url: fantomTestEndpoint,
accounts: [`0x${fantomTestPrivateKey}`]
},
optimismTest: {
url: optimismTestEndpoint,
accounts: [`0x${optimismTestPrivateKey}`]
},
avalancheTest: {
url: avalancheTestEndpoint,
accounts: [`0x${avalancheTestPrivateKey}`]
},
celoAlfajoresTest: {
url: celoAlfajoresTestEndpoint,
accounts: [`0x${celoAlfajoresTestPrivateKey}`]
},
oasisEmeraldTest: {
url: oasisEmeraldTestEndpoint,
accounts: [`0x${oasisEmeraldTestPrivateKey}`]
},
oasisSapphireTest: {
url: oasisSapphireTestEndpoint,
accounts: [`0x${oasisSapphireTestPrivateKey}`]
},
moonbaseAlphaTest: {
url: moonbaseAlphaTestEndpoint,
accounts: [`0x${moonbaseAlphaTestPrivateKey}`]
},
reiTest: {
url: reiTestEndpoint,
accounts: [`0x${reiTestPrivateKey}`]
},
nervosGodwokenTest: {
url: nervosGodwokenTestEndpoint,
accounts: [`0x${nervosGodwokenTestPrivateKey}`]
},
kavaTest: {
url: kavaTestEndpoint,
accounts: [`0x${kavaTestPrivateKey}`]
},
darwiniaPangolinTest: {
url: darwiniaPangolinTestEndpoint,
accounts: [`0x${darwiniaPangolinTestPrivateKey}`]
},
platonTest: {
url: platonTestEndpoint,
accounts: [`0x${platonTestPrivateKey}`]
},
polygonTest: {
url: polygonTestEndpoint,
accounts: [`0x${polygonTestPrivateKey}`]
},
sxTest: {
url: sxTestEndpoint,
accounts: [`0x${sxTestPrivateKey}`]
},
swimmerTest: {
url: swimmerTestEndpoint,
accounts: [`0x${swimmerTestPrivateKey}`]
},
dexalotTest: {
url: dexalotTestEndpoint,
accounts: [`0x${dexalotTestPrivateKey}`]
},
nervosTestnet: {
url: nervosTestnetEndpoint,
accounts: [`0x${nervosTestnetPrivateKey}`]
},
shibuyaTestnet: {
url: shibuyaTestnetEndpoint,
accounts: [`0x${shibuyaTestnetPrivateKey}`]
},
cubeDevnet: {
url: cubeDevnetEndpoint,
accounts: [`0x${cubeDevnetPrivateKey}`]
},
oasysTest: {
url: oasysTestEndpoint,
accounts: [`0x${oasysTestPrivateKey}`]
},
antimatterB2Test: {
url: antimatterB2TestEndpoint,
accounts: [`0x${antimatterB2TestPrivateKey}`]
},
// Mainnets
ethMainnet: getNetworkConfig(ethMainnetEndpoint, kmsKeyId, ethMainnetPrivateKey),
bsc: getNetworkConfig(bscEndpoint, kmsKeyId, bscPrivateKey, 5000000000),
arbitrumOne: getNetworkConfig(arbitrumOneEndpoint, kmsKeyId, arbitrumOnePrivateKey),
arbitrumNova: getNetworkConfig(arbitrumNovaEndpoint, kmsKeyId, arbitrumNovaPrivateKey),
polygon: getNetworkConfig(polygonEndpoint, kmsKeyId, polygonPrivateKey, 50000000000),
fantom: getNetworkConfig(fantomEndpoint, kmsKeyId, fantomPrivateKey),
avalanche: getNetworkConfig(avalancheEndpoint, kmsKeyId, avalanchePrivateKey),
optimism: getNetworkConfig(optimismEndpoint, kmsKeyId, optimismPrivateKey),
boba: getNetworkConfig(bobaEndpoint, kmsKeyId, bobaPrivateKey),
harmony: getNetworkConfig(harmonyEndpoint, kmsKeyId, harmonyPrivateKey),
moonbeam: getNetworkConfig(moonbeamEndpoint, kmsKeyId, moonbeamPrivateKey),
moonriver: getNetworkConfig(moonriverEndpoint, kmsKeyId, moonriverPrivateKey),
celo: getNetworkConfig(celoEndpoint, kmsKeyId, celoPrivateKey),
oasisEmerald: getNetworkConfig(oasisEmeraldEndpoint, kmsKeyId, oasisEmeraldPrivateKey),
oasisSapphire: getNetworkConfig(oasisSapphireEndpoint, kmsKeyId, oasisSapphirePrivateKey),
metis: getNetworkConfig(metisEndpoint, kmsKeyId, metisPrivateKey),
aurora: getNetworkConfig(auroraEndpoint, kmsKeyId, auroraPrivateKey),
xdai: getNetworkConfig(xdaiEndpoint, kmsKeyId, xdaiPrivateKey),
oec: getNetworkConfig(oecEndpoint, kmsKeyId, oecPrivateKey),
heco: getNetworkConfig(hecoEndpoint, kmsKeyId, hecoPrivateKey),
astar: getNetworkConfig(astarEndpoint, kmsKeyId, astarPrivateKey),
shiden: getNetworkConfig(shidenEndpoint, kmsKeyId, shidenPrivateKey),
syscoin: getNetworkConfig(syscoinEndpoint, kmsKeyId, syscoinPrivateKey),
milkomedaC1: getNetworkConfig(milkomedaC1Endpoint, kmsKeyId, milkomedaC1PrivateKey),
milkomedaA1: getNetworkConfig(milkomedaA1Endpoint, kmsKeyId, milkomedaA1PrivateKey),
evmos: getNetworkConfig(evmosEndpoint, kmsKeyId, evmosPrivateKey),
clover: getNetworkConfig(cloverEndpoint, kmsKeyId, cloverPrivateKey),
rei: getNetworkConfig(reiEndpoint, kmsKeyId, reiPrivateKey),
conflux: getNetworkConfig(confluxEndpoint, kmsKeyId, confluxPrivateKey),
darwiniaCrab: getNetworkConfig(darwiniaCrabEndpoint, kmsKeyId, darwiniaCrabPrivateKey),
platon: getNetworkConfig(platonEndpoint, kmsKeyId, platonPrivateKey),
ontology: getNetworkConfig(ontologyEndpoint, kmsKeyId, ontologyPrivateKey),
swimmer: getNetworkConfig(swimmerEndpoint, kmsKeyId, swimmerPrivateKey),
sxNetwork: getNetworkConfig(sxNetworkEndpoint, kmsKeyId, sxNetworkPrivateKey),
ape: getNetworkConfig(apeEndpoint, kmsKeyId, apePrivateKey),
kava: getNetworkConfig(kavaEndpoint, kmsKeyId, kavaPrivateKey),
fncy: getNetworkConfig(fncyEndpoint, kmsKeyId, fncyPrivateKey),
nervosGodwoken: getNetworkConfig(nervosGodwokenEndpoint, kmsKeyId, nervosGodwokenPrivateKey),
klaytn: getNetworkConfig(klaytnEndpoint, kmsKeyId, klaytnPrivateKey, 250000000000),
oasys: getNetworkConfig(oasysEndpoint, kmsKeyId, oasysPrivateKey),
sps: getNetworkConfig(spsEndpoint, kmsKeyId, spsPrivateKey),
fvm: getNetworkConfig(fvmEndpoint, kmsKeyId, fvmPrivateKey),
canto: getNetworkConfig(cantoEndpoint, kmsKeyId, cantoPrivateKey),
polygonZkevm: getNetworkConfig(polygonZkevmEndpoint, kmsKeyId, polygonZkevmPrivateKey),
antimatterB2: getNetworkConfig(antimatterB2Endpoint, kmsKeyId, antimatterB2PrivateKey),
linea: getNetworkConfig(lineaEndpoint, kmsKeyId, lineaPrivateKey),
base: getNetworkConfig(baseEndpoint, kmsKeyId, basePrivateKey),
telos: getNetworkConfig(telosEndpoint, kmsKeyId, telosPrivateKey),
scroll: getNetworkConfig(scrollEndpoint, kmsKeyId, scrollPrivateKey),
zksyncEra: zksyncEraNetwork,
flowEvm: getNetworkConfig(flowEvmEndpoint, kmsKeyId, flowEvmPrivateKey),
},
namedAccounts: {
deployer: {
default: 0
}
},
solidity: {
version: '0.8.17',
settings: {
optimizer: {
enabled: true,
runs: 800
}
}
},
contractSizer: {
alphaSort: true,
runOnCompile: false,
disambiguatePaths: false
},
gasReporter: {
enabled: process.env.REPORT_GAS === 'true' ? true : false,
noColors: true,
outputFile: 'reports/gas_usage/summary.txt'
},
typechain: {
outDir: 'typechain',
target: 'ethers-v6'
},
etherscan: {
apiKey: {
// Testnets
goerli: process.env.ETHERSCAN_API_KEY || '',
holesky: process.env.ETHERSCAN_API_KEY || '',
avalancheFujiTestnet: process.env.SNOWTRACE_API_KEY || '',
bscTestnet: process.env.BSCSCAN_API_KEY || '',
arbitrumTestnet: process.env.ARBISCAN_API_KEY || '',
ftmTestnet: process.env.FTMSCAN_API_KEY || '',
polygonMumbai: process.env.POLYGONSCAN_API_KEY || '',
// Mainnets
mainnet: process.env.ETHERSCAN_API_KEY || '',
avalanche: process.env.SNOWTRACE_API_KEY || '',
bsc: process.env.BSCSCAN_API_KEY || '',
arbitrumOne: process.env.ARBISCAN_API_KEY || '',
optimisticEthereum: process.env.OPTIMISTIC_ETHERSCAN_API_KEY || '',
opera: process.env.FTMSCAN_API_KEY || '',
polygon: process.env.POLYGONSCAN_API_KEY || '',
aurora: process.env.AURORASCAN_API_KEY || '',
moonriver: process.env.MOONRIVER_MOONSCAN_API_KEY || '',
moonbeam: process.env.MOONBEAM_MOONSCAN_API_KEY || '',
heco: process.env.HECOSCAN_API_KEY || '',
arbitrumNova: process.env.ARBISCAN_NOVA_API_KEY || '',
linea: process.env.LINEA_API_KEY || '',
base: process.env.BASE_API_KEY || ''
},
customChains: [
{
network: 'arbitrumNova',
chainId: 42170,
urls: {
apiURL: process.env.ARBITRUM_NOVA_ENDPOINT || '',
browserURL: process.env.ARBITRUM_NOVA_EXPLORER || ''
}
},
{
network: 'linea',
chainId: 59144,
urls: {
apiURL: process.env.LINEA_API_ENDPOINT || '',
browserURL: process.env.LINEA_EXPLORER || ''
}
},
{
network: 'base',
chainId: 8453,
urls: {
apiURL: process.env.BASE_API_ENDPOINT || '',
browserURL: process.env.BASE_EXPLORER || ''
}
}
]
},
zksolc: {
version: 'latest', // optional.
settings: {
//compilerPath: 'zksolc', // optional. Ignored for compilerSource "docker". Can be used if compiler is located in a specific folder
libraries: {}, // optional. References to non-inlinable libraries
isSystem: false, // optional. Enables Yul instructions available only for zkSync system contracts and libraries
forceEvmla: false, // optional. Falls back to EVM legacy assembly if there is a bug with Yul
optimizer: {
enabled: true, // optional. True by default
mode: '3' // optional. 3 by default, z to optimize bytecode size
},
experimental: {
dockerImage: '', // deprecated
tag: '' // deprecated
}
}
}
};
// if (config.networks?.polygon) {
// config.networks.polygon.minMaxPriorityFeePerGas = 30000000000;
// }
// if (config.networks?.fantom) {
// config.networks.fantom.minMaxPriorityFeePerGas = 30000000000;
// }
// if (config.networks?.bsc) {
// config.networks.bsc.minMaxPriorityFeePerGas = 3000000000;
// config.networks.bsc.minMaxFeePerGas = 3000000000;
// }
export default config;
================================================
FILE: import-sorter.json
================================================
{
"generalConfiguration.sortOnBeforeSave": true,
"importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
"importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 120,
"importStringConfiguration.tabSize": 2,
"importStringConfiguration.quoteMark": "single"
}
================================================
FILE: package.json
================================================
{
"name": "sgn-v2-contracts",
"version": "0.2.0",
"description": "SGN V2 Contracts",
"scripts": {
"clean": "hardhat clean",
"compile": "hardhat compile",
"generate:typechain": "rm -rf typechain && hardhat typechain",
"report-gas:summary": "REPORT_GAS=true hardhat test",
"report-gas:benchmark": "hardhat test benchmark/*.ts",
"size-contracts": "hardhat size-contracts | sed -r 's/[[:cntrl:]]\\[[0-9]{1,3}m//g' > reports/contract_sizes.txt",
"test": "hardhat compile && hardhat test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/celer-network/sgn-v2-contracts.git"
},
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/celer-network/sgn-v2-contracts/issues"
},
"homepage": "https://github.com/celer-network/sgn-v2-contracts#readme",
"devDependencies": {
"@aws-sdk/client-kms": "^3.606.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@nomicfoundation/hardhat-ignition": "^0.15.5",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.11",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/ignition-core": "^0.15.5",
"@oasisprotocol/sapphire-hardhat": "^2.19.4",
"@openzeppelin/contracts": "4.5.0",
"@openzeppelin/contracts-upgradeable": "4.5.0",
"@openzeppelin/hardhat-upgrades": "^3.2.0",
"@openzeppelin/upgrades-core": "^1.34.1",
"@typechain/ethers-v5": "^11.1.2",
"@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.7",
"@types/node": "^20.14.9",
"@types/sinon-chai": "^3.2.12",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"chai": "^4.4.1",
"eslint": "^9.6.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"ethers": "^6.13.1",
"fs-extra": "^11.2.0",
"hardhat": "^2.22.5",
"hardhat-contract-sizer": "^2.10.0",
"hardhat-deploy": "^0.12.4",
"hardhat-deploy-ethers": "^0.4.2",
"hardhat-gas-reporter": "^2.2.0",
"husky": "^9.0.11",
"prettier": "^3.3.2",
"prettier-plugin-solidity": "^1.3.1",
"protobufjs": "^7.3.2",
"solc": "0.8.17",
"solidity-coverage": "^0.8.1",
"ts-node": "^10.9.2",
"typechain": "^8.3.2",
"typescript": "^5.5.2"
},
"dependencies": {
"@matterlabs/hardhat-zksync-solc": "^1.2.0",
"@matterlabs/hardhat-zksync-upgradable": "^1.5.1",
"@matterlabs/hardhat-zksync-verify": "^1.5.0",
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-verify": "^2.0.8",
"dotenv": "^16.4.5",
"hardhat-signer-kms": "^1.2.1"
}
}
================================================
FILE: reports/contract_sizes.txt
================================================
Nothing to compile
No need to generate any newer typings.
·---------------------------------------------|-------------|---------------·
| Contract Name · Size (KB) · Change (KB) │
··············································|·············|················
| Address · 0.086 · │
··············································|·············|················
| BatchTransfer · 9.934 · │
··············································|·············|················
| Bridge · 20.788 · │
··············································|·············|················
| BridgeTransferLib · 0.086 · │
··············································|·············|················
| ContractAsLP · 5.276 · │
··············································|·············|················
| ContractAsSender · 15.679 · │
··············································|·············|················
| Counters · 0.086 · │
··············································|·············|················
| CrossChainSwap · 8.237 · │
··············································|·············|················
| DataTypes · 0.086 · │
··············································|·············|················
| DummySwap · 2.184 · │
··············································|·············|················
| ECDSA · 0.086 · │
··············································|·············|················
| ERC20 · 2.346 · │
··············································|·············|················
| ERC721 · 5.021 · │
··············································|·············|················
| FarmingRewards · 6.980 · │
··············································|·············|················
| Faucet · 2.730 · │
··············································|·············|················
| FraxBridgeToken · 5.697 · │
··············································|·············|················
| Govern · 5.242 · │
··············································|·············|················
| GovernedOwnerProxy · 10.598 · │
··············································|·············|················
| IntermediaryBridgeToken · 5.375 · │
··············································|·············|················
| IntermediaryOriginalToken · 5.077 · │
··············································|·············|················
| MaiBridgeToken · 5.976 · │
··············································|·············|················
| MCNNFT · 10.483 · │
··············································|·············|················
| MessageBus · 16.196 · │
··············································|·············|················
| MessageBusReceiver · 13.034 · │
··············································|·············|················
| MessageBusSender · 4.219 · │
··············································|·············|················
| MessageSenderLib · 0.086 · │
··············································|·············|················
| MintableERC20 · 3.745 · │
··············································|·············|················
| MintSwapCanonicalToken · 5.988 · │
··············································|·············|················
| MintSwapCanonicalTokenPermit · 7.722 · │
··············································|·············|················
| MintSwapCanonicalTokenUpgradable · 6.691 · │
··············································|·············|················
| MintSwapCanonicalTokenUpgradableFreezable · 7.483 · │
··············································|·············|················
| MsgDataTypes · 0.086 · │
··············································|·············|················
| MsgExampleBasic · 2.991 · │
··············································|·············|················
| MsgExampleBasicTransfer · 7.901 · │
··············································|·············|················
| MsgExampleInOrder · 3.725 · │
··············································|·············|················
| MsgTest · 10.301 · │
··············································|·············|················
| MultiBridgeToken · 4.317 · │
··············································|·············|················
| MultiBridgeTokenPermit · 6.051 · │
··············································|·············|················
| NFTBridge · 15.231 · │
··············································|·············|················
| OntologyBridgeToken · 5.925 · │
··············································|·············|················
| OriginalTokenVault · 13.441 · │
··············································|·············|················
| OriginalTokenVaultV2 · 13.488 · │
··············································|·············|················
| OrigNFT · 6.809 · │
··············································|·············|················
| OwnerDataTypes · 0.086 · │
··············································|·············|················
| Pb · 0.086 · │
··············································|·············|················
| PbBridge · 0.086 · │
··············································|·············|················
| PbFarming · 0.086 · │
··············································|·············|················
| PbPegged · 0.086 · │
··············································|·············|················
| PbPool · 0.086 · │
··············································|·············|················
| PbSgn · 0.086 · │
··············································|·············|················
| PbStaking · 0.086 · │
··············································|·············|················
| PeggedTokenBridge · 10.885 · │
··············································|·············|················
| PeggedTokenBridgeV2 · 12.104 · │
··············································|·············|················
| PegNFT · 6.952 · │
··············································|·············|················
| Pool · 16.467 · │
··············································|·············|················
| RestrictedMultiBridgeTokenOwner · 1.682 · │
··············································|·············|················
| RFQ · 18.534 · │
··············································|·············|················
| SafeERC20 · 0.086 · │
··············································|·············|················
| SGN · 7.973 · │
··············································|·············|················
| Signers · 5.525 · │
··············································|·············|················
| SimpleGovernance · 10.223 · │
··············································|·············|················
| SingleBridgeToken · 4.131 · │
··············································|·············|················
| SingleBridgeTokenPermit · 5.865 · │
··············································|·············|················
| Staking · 24.325 · │
··············································|·············|················
| StakingReward · 6.114 · │
··············································|·············|················
| Strings · 0.086 · │
··············································|·············|················
| SwapBridgeToken · 5.961 · │
··············································|·············|················
| TestERC20 · 2.376 · │
··············································|·············|················
| TransferAgent · 6.955 · │
··············································|·············|················
| TransferSwap · 12.189 · │
··············································|·············|················
| Viewer · 5.636 · │
··············································|·············|················
| WETH · 3.336 · │
··············································|·············|················
| WithdrawInbox · 2.547 · │
··············································|·············|················
| WrappedBridgeToken · 5.754 · │
·---------------------------------------------|-------------|---------------·
================================================
FILE: reports/gas_usage/relay.txt
================================================
for cbr testErc20 relay tx
5 3 169749
7 3 140538
7 5 151548
9 3 142563
9 5 156553
9 7 167603
11 3 150373
11 5 161415
11 7 169631
13 3 155344
13 5 166299
13 7 177433
13 9 188496
15 3 157390
15 5 171304
15 7 182267
15 9 193458
15 11 204450
17 3 162350
17 5 176260
17 7 187255
17 9 198338
17 11 209362
19 3 167237
19 5 181220
19 7 192139
19 9 203346
19 11 214350
19 13 222556
21 3 175015
21 5 186070
21 7 197201
21 9 208149
21 11 216499
21 13 230435
21 15 241492
per sig cost: 5540
12 8 180470
13 8 182962
14 8 182576
15 8 187980
16 8 190284
17 8 192816
18 8 195284
19 8 194926
20 8 200142
21 8 202690
per validator cost: 2469
================================================
FILE: reports/gas_usage/summary.txt
================================================
·------------------------------------------------------|---------------------------|-------------|-----------------------------·
| Solc version: 0.8.17 · Optimizer enabled: true · Runs: 800 · Block limit: 30000000 gas │
·······················································|···························|·············|······························
| Methods │
·······················|·······························|·············|·············|·············|···············|··············
| Contract · Method · Min · Max · Avg · # calls · eur (avg) │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · addLiquidity · - · - · 94648 · 5 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · executeDelayedTransfer · - · - · 54503 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · relay · 117698 · 188532 · 150251 · 11 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · resetSigners · 73421 · 77571 · 76188 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · send · - · - · 84544 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setDelayPeriod · - · - · 46876 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setDelayThresholds · - · - · 49436 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setEpochLength · - · - · 46909 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setEpochVolumeCaps · - · - · 49478 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setMaxSend · - · - · 49457 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · setMinAdd · - · - · 49433 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · transferOwnership · 28650 · 28662 · 28660 · 5 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · updateSigners · 67619 · 72217 · 69918 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Bridge · withdraw · - · - · 114249 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| DummySwap · setFakeSlippage · - · - · 26428 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| FarmingRewards · claimRewards · 112992 · 130084 · 121538 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| FarmingRewards · contributeToRewardPool · 50400 · 67500 · 64389 · 11 · - │
·······················|·······························|·············|·············|·············|···············|··············
| FarmingRewards · pause · - · - · 29783 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Govern · confirmParamProposal · 98331 · 99264 · 98798 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Govern · createParamProposal · - · - · 229948 · 10 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Govern · voteParam · - · - · 56052 · 10 · - │
·······················|·······························|·············|·············|·············|···············|··············
| GovernedOwnerProxy · initGov · 46215 · 46227 · 46225 · 5 · - │
·······················|·······························|·············|·············|·············|···············|··············
| GovernedOwnerProxy · proposeResetSigners · - · - · 136167 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| GovernedOwnerProxy · proposeTransferOwnership · - · - · 113155 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| GovernedOwnerProxy · proposeUpdateGovernor · - · - · 113926 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| GovernedOwnerProxy · proposeUpdatePauser · 113988 · 131050 · 122519 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| MessageBus · executeMessage · 83923 · 85618 · 84891 · 9 · - │
·······················|·······························|·············|·············|·············|···············|··············
| MessageBus · executeMessage · - · - · 85001 · 3 · - │
·······················|·······························|·············|·············|·············|···············|··············
| MessageBus · transferAndExecuteMsg · - · - · 175691 · 3 · - │
·······················|·······························|·············|·············|·············|···············|··············
| PeggedTokenBridge · mint · - · - · 154806 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| PeggedTokenBridge · transferOwnership · 28639 · 28651 · 28649 · 5 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SGN · updateSgnAddr · 49524 · 85921 · 82399 · 31 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · createParamChangeProposal · - · - · 121155 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · createProxyUpdateProposal · - · - · 111384 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · createTransferTokenProposal · - · - · 122482 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · createVoterUpdateProposal · - · - · 127937 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · executeProposal · 88875 · 138438 · 113083 · 18 · - │
·······················|·······························|·············|·············|·············|···············|··············
| SimpleGovernance · voteProposal · 49931 · 49943 · 49935 · 6 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · addWhitelisted · - · - · 47514 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · bondValidator · 86379 · 137685 · 101835 · 206 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · collectForfeiture · - · - · 57812 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · completeUndelegate · 57653 · 68124 · 62431 · 10 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · confirmUnbondedValidator · - · - · 33838 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · delegate · 69552 · 86664 · 75270 · 256 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · drainToken · - · - · 40813 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · initializeValidator · 244311 · 282523 · 256848 · 214 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · pause · - · - · 29806 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · setGovContract · 46153 · 46165 · 46164 · 9 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · setMaxSlashFactor · - · - · 23898 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · setRewardContract · - · - · 46188 · 6 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · setWhitelistEnabled · - · - · 45892 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · slash · 148219 · 227830 · 183068 · 16 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · undelegateShares · 54778 · 189800 · 135745 · 21 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · undelegateTokens · 100199 · 112066 · 104155 · 3 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · unpause · - · - · 29777 · 1 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · updateCommissionRate · - · - · 32393 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · updateMinSelfDelegation · 34512 · 41901 · 36623 · 7 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Staking · updateValidatorSigner · 53445 · 55683 · 54564 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| StakingReward · claimReward · 102379 · 119459 · 110919 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| StakingReward · contributeToRewardPool · 52719 · 69819 · 66710 · 11 · - │
·······················|·······························|·············|·············|·············|···············|··············
| StakingReward · pause · - · - · 29783 · 2 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TestERC20 · approve · 46283 · 46295 · 46294 · 398 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TestERC20 · transfer · 51555 · 51579 · 51577 · 461 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · executeMessageWithTransfer · 45082 · 122433 · 94105 · 8 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · setMessageBus · - · - · 30056 · 4 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · setMinSwapAmount · - · - · 46338 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · setSupportedDex · 26516 · 26528 · 26526 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · transferWithSwap · 136832 · 228452 · 171209 · 10 · - │
·······················|·······························|·············|·············|·············|···············|··············
| TransferSwap · transferWithSwapNative · - · - · 228511 · 3 · - │
·······················|·······························|·············|·············|·············|···············|··············
| WETH · deposit · - · - · 67647 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| WETH · transfer · 46767 · 46779 · 46777 · 12 · - │
·······················|·······························|·············|·············|·············|···············|··············
| Deployments · · % of limit · │
·······················································|·············|·············|·············|···············|··············
| Bridge · - · - · 4642238 · 15.5 % · - │
·······················································|·············|·············|·············|···············|··············
| DummySwap · - · - · 569089 · 1.9 % · - │
·······················································|·············|·············|·············|···············|··············
| FarmingRewards · 1611954 · 1611966 · 1611965 · 5.4 % · - │
·······················································|·············|·············|·············|···············|··············
| Govern · 1184346 · 1184370 · 1184367 · 3.9 % · - │
·······················································|·············|·············|·············|···············|··············
| GovernedOwnerProxy · - · - · 2365331 · 7.9 % · - │
·······················································|·············|·············|·············|···············|··············
| MessageBus · - · - · 3611043 · 12 % · - │
·······················································|·············|·············|·············|···············|··············
| MsgTest · - · - · 2325332 · 7.8 % · - │
·······················································|·············|·············|·············|···············|··············
| PeggedTokenBridge · - · - · 2483141 · 8.3 % · - │
·······················································|·············|·············|·············|···············|··············
| SGN · 1825005 · 1825017 · 1825016 · 6.1 % · - │
·······················································|·············|·············|·············|···············|··············
| SimpleGovernance · - · - · 2587562 · 8.6 % · - │
·······················································|·············|·············|·············|···············|··············
| SingleBridgeToken · 1051856 · 1051868 · 1051867 · 3.5 % · - │
·······················································|·············|·············|·············|···············|··············
| Staking · 5537490 · 5537502 · 5537500 · 18.5 % · - │
·······················································|·············|·············|·············|···············|··············
| StakingReward · 1424076 · 1424088 · 1424087 · 4.7 % · - │
·······················································|·············|·············|·············|···············|··············
| TestERC20 · - · - · 668912 · 2.2 % · - │
·······················································|·············|·············|·············|···············|··············
| TransferSwap · 2778901 · 2778925 · 2778922 · 9.3 % · - │
·······················································|·············|·············|·············|···············|··············
| Viewer · 1268098 · 1268110 · 1268109 · 4.2 % · - │
·······················································|·············|·············|·············|···············|··············
| WETH · - · - · 824506 · 2.7 % · - │
·------------------------------------------------------|-------------|-------------|-------------|---------------|-------------·
================================================
FILE: scripts/common.ts
================================================
import { Numeric, Overrides, parseUnits } from 'ethers';
import { ethers, getNamedAccounts } from 'hardhat';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
export async function getDeployerSigner(): Promise {
const deployer = (await getNamedAccounts())['deployer'];
return await ethers.getSigner(deployer);
}
export async function getFeeOverrides(): Promise {
const feeData = await ethers.provider.getFeeData();
// const network = await ethers.provider.getNetwork();
// if (network.chainId == BigInt(59144)) {
// // for Linea
// return { maxFeePerGas: 5000000000, maxPriorityFeePerGas: 4900000000 };
// }
if (feeData.maxFeePerGas) {
return { maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas || 0 };
}
return { gasPrice: feeData.gasPrice || 0 };
}
export function getParseUnitsCallback(
unitNames: (string | Numeric)[]
): (value: string, index: number, array: string[]) => bigint {
return (s, i) => parseUnits(s, unitNames[i]);
}
================================================
FILE: scripts/init_governed_owner.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { GovernedOwnerProxy__factory } from '../typechain';
import { getDeployerSigner, getFeeOverrides } from './common';
dotenv.config();
async function initGov(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const ownerProxyAddr = process.env.GOVERNED_OWNER_PROXY as string;
if (!ownerProxyAddr) {
return;
}
const simpleGovernanceAddr = process.env.SIMPLE_GOVERNANCE as string;
if (!simpleGovernanceAddr) {
return;
}
const governedOwnerProxy = GovernedOwnerProxy__factory.connect(ownerProxyAddr, deployerSigner);
await (await governedOwnerProxy.initGov(simpleGovernanceAddr, feeOverrides)).wait();
console.log('initGov', ownerProxyAddr, simpleGovernanceAddr);
}
initGov();
================================================
FILE: scripts/oasys_token_factory.ts
================================================
import * as dotenv from 'dotenv';
import { AbiCoder, parseUnits } from 'ethers';
import { L1StandardERC20__factory, L1StandardERC20Factory__factory } from '../typechain';
import { getDeployerSigner } from './common';
dotenv.config();
async function createToken(): Promise {
const abiCoder = AbiCoder.defaultAbiCoder();
const deployerSigner = await getDeployerSigner();
const pegbrV2Addr = process.env.PEGGED_TOKEN_BRIDGE as string;
const factoryAddr = process.env.OASYS_TOKEN_FACTORY as string;
const tokenName = process.env.OASYS_FACTORY_TOKEN_NAME as string;
const tokenSymbol = process.env.OASYS_FACTORY_TOKEN_SYMBOL as string;
console.log(
'params',
'\nPEGGED_TOKEN_BRIDGE',
pegbrV2Addr,
'\nOASYS_TOKEN_FACTORY',
factoryAddr,
'\nOASYS_FACTORY_TOKEN_NAME',
tokenName,
'\nOASYS_FACTORY_TOKEN_SYMBOL',
tokenSymbol
);
console.log('creating ERC20 token, calling token factory', factoryAddr);
const factory = L1StandardERC20Factory__factory.connect(factoryAddr, deployerSigner);
const tx = await factory.createStandardERC20(tokenName, tokenSymbol, {
gasLimit: 5_000_000,
maxFeePerGas: parseUnits('10', 'gwei'),
maxPriorityFeePerGas: parseUnits('8', 'gwei')
});
console.log('tx hash', tx.hash, 'nonce', tx.nonce, 'waiting for 5 block confirmations...');
await tx.wait(5);
console.log('deployed, finding tx receipt for logs...');
const receipt = (await deployerSigner.provider.getTransactionReceipt(tx.hash))!;
const log = receipt.logs.find((log) => log.address === factoryAddr);
if (!log || log.topics.length !== 3) {
console.error('token creation log not found');
process.exit(1);
}
const tokenAddr = abiCoder.decode(['address'], log.topics[2]).toString();
console.log('deployed token address', tokenAddr);
const token = L1StandardERC20__factory.connect(tokenAddr, deployerSigner);
const minterRole = await token.MINTER_ROLE();
console.log('adding minter role', minterRole);
const setRoleTx = await token.grantRole(minterRole, pegbrV2Addr, {
gasLimit: 5_000_000,
nonce: tx.nonce + 1,
maxFeePerGas: parseUnits('10', 'gwei'),
maxPriorityFeePerGas: parseUnits('8', 'gwei')
});
console.log('tx hash', setRoleTx.hash);
await setRoleTx.wait(2);
}
createToken();
// ABI of https://github.com/oasysgames/oasys-optimism/blob/develop/packages/contracts/contracts/oasys/L1/token/L1StandardERC20Factory.sol
// [
// {
// "anonymous": false,
// "inputs": [
// {
// "indexed": true,
// "internalType": "string",
// "name": "_symbol",
// "type": "string"
// },
// {
// "indexed": true,
// "internalType": "address",
// "name": "_address",
// "type": "address"
// }
// ],
// "name": "ERC20Created",
// "type": "event"
// },
// {
// "inputs": [
// {
// "internalType": "string",
// "name": "_name",
// "type": "string"
// },
// {
// "internalType": "string",
// "name": "_symbol",
// "type": "string"
// }
// ],
// "name": "createStandardERC20",
// "outputs": [],
// "stateMutability": "nonpayable",
// "type": "function"
// }
// ]
================================================
FILE: scripts/pb_gen_sol.sh
================================================
PROTOC_VER=""
GEN_SOL_VER=""
# need to install and run npx prettier -w contracts
prepare_tools() {
setup_git
}
gen_sol() {
echo "TBD"
}
add_to_pr() {
echo "TBD"
}
setup_git() {
git config --global user.email "build@celer.network"
git config --global user.name "Build Bot"
git config --global push.default "current"
}
================================================
FILE: scripts/reset_signers.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Bridge__factory } from '../typechain';
import { getDeployerSigner, getFeeOverrides } from './common';
dotenv.config();
async function resetSigners(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const bridgeAddr = process.env.BRIDGE as string;
if (!bridgeAddr) {
return;
}
const bridge = Bridge__factory.connect(bridgeAddr, deployerSigner);
// Uncomment if needed
// await (await bridge.notifyResetSigners(feeOverrides)).wait();
const signers = (process.env.BRIDGE_SIGNERS as string).split(',');
const powers = (process.env.BRIDGE_POWERS as string).split(',');
await (await bridge.resetSigners(signers, powers, feeOverrides)).wait();
console.log('resetSigners', signers, powers);
}
resetSigners();
================================================
FILE: scripts/sentinel/sentinel_guard.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Sentinel__factory } from '../../typechain';
import { getDeployerSigner, getFeeOverrides } from '../common';
dotenv.config();
async function guard(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const sentinelAddr = process.env.SENTINEL as string;
if (!sentinelAddr) {
return;
}
const sentinel = Sentinel__factory.connect(sentinelAddr, deployerSigner);
await (await sentinel.guard(feeOverrides)).wait();
}
guard();
================================================
FILE: scripts/sentinel/sentinel_relax.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Sentinel__factory } from '../../typechain';
import { getDeployerSigner, getFeeOverrides } from '../common';
dotenv.config();
async function relax(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const sentinelAddr = process.env.SENTINEL as string;
if (!sentinelAddr) {
return;
}
const sentinel = Sentinel__factory.connect(sentinelAddr, deployerSigner);
await (await sentinel.relax(feeOverrides)).wait();
}
relax();
================================================
FILE: scripts/sentinel/sentinel_set_limits.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Sentinel__factory } from '../../typechain';
import { TypedContractMethod } from '../../typechain/common';
import { getDeployerSigner, getFeeOverrides, getParseUnitsCallback } from '../common';
import type { AddressLike, BigNumberish, Overrides } from 'ethers';
dotenv.config();
async function setLimitIfSpecified(
limitEnv: string,
target: string,
tokens: string[],
decimals: string[],
methodName: string,
method: TypedContractMethod<
[_target: string, _tokens: AddressLike[], _amounts: BigNumberish[]],
[void],
'nonpayable'
>,
feeOverrides: Overrides
): Promise {
if (limitEnv) {
const limitStr = limitEnv.split(',');
if (limitEnv.length > 0 && limitStr.length === decimals.length) {
const limits = limitStr.map(getParseUnitsCallback(decimals.map(Number)));
await (await method(target, tokens, limits, feeOverrides)).wait();
console.log(
methodName,
target,
tokens,
limits.map((limit) => limit.toString())
);
}
}
}
async function setBridgeLimits(sentinelAddr: string): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const bridgeAddr = process.env.BRIDGE as string;
if (!bridgeAddr) {
return;
}
const tokensStr = process.env.BRIDGE_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const sentinel = Sentinel__factory.connect(sentinelAddr, deployerSigner);
const tokens = (process.env.BRIDGE_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.BRIDGE_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MIN_ADDS as string,
bridgeAddr,
tokens,
decimals,
'setMinAdd',
sentinel.setMinAdd,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MIN_SENDS as string,
bridgeAddr,
tokens,
decimals,
'setMinSend',
sentinel.setMinSend,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MAX_SENDS as string,
bridgeAddr,
tokens,
decimals,
'setMaxSend',
sentinel.setMaxSend,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_EPOCH_VOLUME_CAPS as string,
bridgeAddr,
tokens,
decimals,
'setEpochVolumeCaps',
sentinel.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_DELAY_THRESHOLDS as string,
bridgeAddr,
tokens,
decimals,
'setDelayThresholds',
sentinel.setDelayThresholds,
feeOverrides
);
}
async function setOriginalTokenVaultLimits(sentinelAddr: string): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const originalTokenVaultAddr = process.env.ORIGINAL_TOKEN_VAULT as string;
if (!originalTokenVaultAddr) {
return;
}
const tokensStr = process.env.ORIGINAL_TOKEN_VAULT_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const sentinel = Sentinel__factory.connect(sentinelAddr, deployerSigner);
const tokens = (process.env.ORIGINAL_TOKEN_VAULT_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.ORIGINAL_TOKEN_VAULT_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_MIN_DEPOSITS as string,
originalTokenVaultAddr,
tokens,
decimals,
'setMinDeposit',
sentinel.setMinDeposit,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_MAX_DEPOSITS as string,
originalTokenVaultAddr,
tokens,
decimals,
'setMaxDeposit',
sentinel.setMaxDeposit,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_EPOCH_VOLUME_CAPS as string,
originalTokenVaultAddr,
tokens,
decimals,
'setEpochVolumeCaps',
sentinel.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_DELAY_THRESHOLDS as string,
originalTokenVaultAddr,
tokens,
decimals,
'setDelayThresholds',
sentinel.setDelayThresholds,
feeOverrides
);
}
async function setPeggedTokenBridgeLimits(sentinelAddr: string): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const peggedTokenBridgeAddr = process.env.PEGGED_TOKEN_BRIDGE as string;
if (!peggedTokenBridgeAddr) {
return;
}
const tokensStr = process.env.PEGGED_TOKEN_BRIDGE_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const sentinel = Sentinel__factory.connect(sentinelAddr, deployerSigner);
const tokens = (process.env.PEGGED_TOKEN_BRIDGE_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.PEGGED_TOKEN_BRIDGE_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_MIN_BURNS as string,
peggedTokenBridgeAddr,
tokens,
decimals,
'setMinBurn',
sentinel.setMinBurn,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_MAX_BURNS as string,
peggedTokenBridgeAddr,
tokens,
decimals,
'setMaxBurn',
sentinel.setMaxBurn,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_EPOCH_VOLUME_CAPS as string,
peggedTokenBridgeAddr,
tokens,
decimals,
'setEpochVolumeCaps',
sentinel.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_DELAY_THRESHOLDS as string,
peggedTokenBridgeAddr,
tokens,
decimals,
'setDelayThresholds',
sentinel.setDelayThresholds,
feeOverrides
);
}
async function setLimits(): Promise {
const sentinelAddr = process.env.SENTINEL as string;
if (!sentinelAddr) {
return;
}
await setBridgeLimits(sentinelAddr);
await setOriginalTokenVaultLimits(sentinelAddr);
await setPeggedTokenBridgeLimits(sentinelAddr);
}
setLimits();
================================================
FILE: scripts/set_basics.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import {
Bridge__factory,
MessageBus__factory,
OriginalTokenVault__factory,
PeggedNativeTokenBridge__factory,
PeggedTokenBridge__factory,
RFQ__factory
} from '../typechain';
import { getDeployerSigner, getFeeOverrides } from './common';
dotenv.config();
async function setBridgeBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const bridgeAddr = process.env.BRIDGE as string;
if (!bridgeAddr) {
return;
}
const bridge = Bridge__factory.connect(bridgeAddr, deployerSigner);
const pausers = (process.env.BRIDGE_PAUSERS as string).split(',');
if (pausers[0].length > 0) {
for (let i = 0; i < pausers.length; i++) {
const pauser = pausers[i];
await (await bridge.addPauser(pauser, feeOverrides)).wait();
console.log('addPauser', pauser);
}
}
const governors = (process.env.BRIDGE_GOVERNORS as string).split(',');
if (governors[0].length > 0) {
for (let i = 0; i < governors.length; i++) {
const governor = governors[i];
await (await bridge.addGovernor(governor, feeOverrides)).wait();
console.log('addGovernor', governor);
}
}
const delayPeriod = process.env.BRIDGE_DELAY_PERIOD as string;
if (delayPeriod) {
await (await bridge.setDelayPeriod(delayPeriod, feeOverrides)).wait();
console.log('setDelayPeriod', delayPeriod);
}
const epochLength = process.env.BRIDGE_EPOCH_LENGTH as string;
if (epochLength) {
await (await bridge.setEpochLength(epochLength, feeOverrides)).wait();
console.log('setEpochLength', epochLength);
}
const minimalMaxSlippage = process.env.BRIDGE_MINIMAL_MAX_SLIPPAGE as string;
if (minimalMaxSlippage) {
await (await bridge.setMinimalMaxSlippage(minimalMaxSlippage, feeOverrides)).wait();
console.log('setMinimalMaxSlippage', minimalMaxSlippage);
}
const weth = process.env.BRIDGE_WETH as string;
if (weth) {
await (await bridge.setWrap(weth, feeOverrides)).wait();
console.log('setWrap', weth);
}
}
async function setPeggedTokenBridgeBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const peggedTokenBridgeAddr = process.env.PEGGED_TOKEN_BRIDGE as string;
if (!peggedTokenBridgeAddr) {
return;
}
const peggedTokenBridge = PeggedTokenBridge__factory.connect(peggedTokenBridgeAddr, deployerSigner);
const pausers = (process.env.PEGGED_TOKEN_BRIDGE_PAUSERS as string).split(',');
if (pausers[0].length > 0) {
for (let i = 0; i < pausers.length; i++) {
const pauser = pausers[i];
await (await peggedTokenBridge.addPauser(pauser, feeOverrides)).wait();
console.log('addPauser', pauser);
}
}
const governors = (process.env.PEGGED_TOKEN_BRIDGE_GOVERNORS as string).split(',');
if (governors[0].length > 0) {
for (let i = 0; i < governors.length; i++) {
const governor = governors[i];
await (await peggedTokenBridge.addGovernor(governor, feeOverrides)).wait();
console.log('addGovernor', governor);
}
}
const delayPeriod = process.env.PEGGED_TOKEN_BRIDGE_DELAY_PERIOD as string;
if (delayPeriod) {
await (await peggedTokenBridge.setDelayPeriod(delayPeriod, feeOverrides)).wait();
console.log('setDelayPeriod', delayPeriod);
}
const epochLength = process.env.PEGGED_TOKEN_BRIDGE_EPOCH_LENGTH as string;
if (epochLength) {
await (await peggedTokenBridge.setEpochLength(epochLength, feeOverrides)).wait();
console.log('setEpochLength', epochLength);
}
}
async function setPeggedNativeTokenBridgeBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const peggedTokenBridgeAddr = process.env.PEGGED_TOKEN_BRIDGE as string;
if (!peggedTokenBridgeAddr) {
return;
}
const peggedNativeTokenBridge = PeggedNativeTokenBridge__factory.connect(peggedTokenBridgeAddr, deployerSigner);
const nativeVault = process.env.PEGGED_TOKEN_BRIDGE_NATIVE_VAULT as string;
if (nativeVault) {
await (await peggedNativeTokenBridge.setNativeVault(nativeVault, feeOverrides)).wait();
console.log('setNativeVault', nativeVault);
}
}
async function setOriginalTokenVaultBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const originalTokenVaultAddr = process.env.ORIGINAL_TOKEN_VAULT as string;
if (!originalTokenVaultAddr) {
return;
}
const originalTokenVault = OriginalTokenVault__factory.connect(originalTokenVaultAddr, deployerSigner);
const pausers = (process.env.ORIGINAL_TOKEN_VAULT_PAUSERS as string).split(',');
if (pausers[0].length > 0) {
for (let i = 0; i < pausers.length; i++) {
const pauser = pausers[i];
await (await originalTokenVault.addPauser(pauser, feeOverrides)).wait();
console.log('addPauser', pauser);
}
}
const governors = (process.env.ORIGINAL_TOKEN_VAULT_GOVERNORS as string).split(',');
if (governors[0].length > 0) {
for (let i = 0; i < governors.length; i++) {
const governor = governors[i];
await (await originalTokenVault.addGovernor(governor, feeOverrides)).wait();
console.log('addGovernor', governor);
}
}
const delayPeriod = process.env.ORIGINAL_TOKEN_VAULT_DELAY_PERIOD as string;
if (delayPeriod) {
await (await originalTokenVault.setDelayPeriod(delayPeriod, feeOverrides)).wait();
console.log('setDelayPeriod', delayPeriod);
}
const epochLength = process.env.ORIGINAL_TOKEN_VAULT_EPOCH_LENGTH as string;
if (epochLength) {
await (await originalTokenVault.setEpochLength(epochLength, feeOverrides)).wait();
console.log('setEpochLength', epochLength);
}
const weth = process.env.ORIGINAL_TOKEN_VAULT_WETH as string;
if (weth) {
await (await originalTokenVault.setWrap(weth, feeOverrides)).wait();
console.log('setWrap', weth);
}
}
async function setMessageBusBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const messageBusAddr = process.env.MESSAGE_BUS as string;
if (!messageBusAddr) {
return;
}
const messageBus = MessageBus__factory.connect(messageBusAddr, deployerSigner);
const liquidityBridge = process.env.MESSAGE_BUS_LIQUIDITY_BRIDGE as string;
if (liquidityBridge) {
await (await messageBus.setLiquidityBridge(liquidityBridge, feeOverrides)).wait();
console.log('setLiquidityBridge', liquidityBridge);
}
const pegVault = process.env.MESSAGE_BUS_PEG_VAULT as string;
if (pegVault) {
await (await messageBus.setPegVault(pegVault, feeOverrides)).wait();
console.log('setPegVault', pegVault);
}
const pegBridge = process.env.MESSAGE_BUS_PEG_BRIDGE as string;
if (pegBridge) {
await (await messageBus.setPegBridge(pegBridge, feeOverrides)).wait();
console.log('setPegBridge', pegBridge);
}
const pegVaultV2 = process.env.MESSAGE_BUS_PEG_VAULT_V2 as string;
if (pegVaultV2) {
await (await messageBus.setPegVaultV2(pegVaultV2, feeOverrides)).wait();
console.log('setPegVaultV2', pegVaultV2);
}
const pegBridgeV2 = process.env.MESSAGE_BUS_PEG_BRIDGE_V2 as string;
if (pegBridgeV2) {
await (await messageBus.setPegBridgeV2(pegBridgeV2, feeOverrides)).wait();
console.log('setPegBridgeV2', pegBridgeV2);
}
}
async function setRFQBasics(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const rfqAddr = process.env.RFQ_ADDR as string;
if (!rfqAddr) {
return;
}
const rfq = RFQ__factory.connect(rfqAddr, deployerSigner);
const pausers = (process.env.RFQ_PAUSERS as string).split(',');
if (pausers[0].length > 0) {
for (let i = 0; i < pausers.length; i++) {
const pauser = pausers[i];
await (await rfq.addPauser(pauser, feeOverrides)).wait();
console.log('addPauser', pauser);
}
}
const governors = (process.env.RFQ_GOVERNORS as string).split(',');
if (governors[0].length > 0) {
for (let i = 0; i < governors.length; i++) {
const governor = governors[i];
await (await rfq.addGovernor(governor, feeOverrides)).wait();
console.log('addGovernor', governor);
}
}
const remoteChains = (process.env.RFQ_REMOTE_CHAINS as string).split(',');
const remoteRFQs = (process.env.RFQ_REMOTE_RFQS as string).split(',');
if (remoteChains[0].length > 0 && remoteChains.length == remoteRFQs.length) {
await (await rfq.setRemoteRfqContracts(remoteChains, remoteRFQs, feeOverrides)).wait();
console.log('setRemoteRfqContracts', remoteChains, remoteRFQs);
}
const feeChains = (process.env.RFQ_FEE_CHAINS as string).split(',');
const feePercs = (process.env.RFQ_FEE_PERCS as string).split(',');
if (feeChains[0].length > 0 && feeChains.length == feePercs.length) {
await (await rfq.setFeePerc(feeChains, feePercs, feeOverrides)).wait();
console.log('setFeePerc', feeChains, feePercs);
}
const treasuryAddr = process.env.RFQ_TREASURE_ADDR as string;
if (treasuryAddr) {
await (await rfq.setTreasuryAddr(treasuryAddr, feeOverrides)).wait();
console.log('setTreasuryAddr', treasuryAddr);
}
const weth = process.env.RFQ_NATIVE_WRAP as string;
if (weth) {
await (await rfq.setNativeWrap(weth, feeOverrides)).wait();
console.log('setNativeWrap', weth);
}
}
async function setBasics(): Promise {
await setBridgeBasics();
await setOriginalTokenVaultBasics();
await setPeggedTokenBridgeBasics();
await setMessageBusBasics();
await setPeggedNativeTokenBridgeBasics();
await setRFQBasics();
}
setBasics();
================================================
FILE: scripts/set_limits.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Bridge__factory, OriginalTokenVault__factory, PeggedTokenBridge__factory } from '../typechain';
import { TypedContractMethod } from '../typechain/common';
import { getDeployerSigner, getFeeOverrides, getParseUnitsCallback } from './common';
import type { AddressLike, BigNumberish, Overrides } from 'ethers';
dotenv.config();
async function setLimitIfSpecified(
limitEnv: string,
tokens: string[],
decimals: string[],
methodName: string,
method: TypedContractMethod<[_tokens: AddressLike[], _amounts: BigNumberish[]], [void], 'nonpayable'>,
feeOverrides: Overrides
): Promise {
if (limitEnv) {
const limitStr = limitEnv.split(',');
if (limitEnv.length > 0 && limitStr.length === decimals.length) {
const limits = limitStr.map(getParseUnitsCallback(decimals.map(Number)));
await (await method(tokens, limits, feeOverrides)).wait();
console.log(
methodName,
tokens,
limits.map((limit) => limit.toString())
);
}
}
}
async function setBridgeLimits(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const bridgeAddr = process.env.BRIDGE as string;
if (!bridgeAddr) {
return;
}
const tokensStr = process.env.BRIDGE_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const bridge = Bridge__factory.connect(bridgeAddr, deployerSigner);
const tokens = (process.env.BRIDGE_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.BRIDGE_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MIN_ADDS as string,
tokens,
decimals,
'setMinAdd',
bridge.setMinAdd,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MIN_SENDS as string,
tokens,
decimals,
'setMinSend',
bridge.setMinSend,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_MAX_SENDS as string,
tokens,
decimals,
'setMaxSend',
bridge.setMaxSend,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_EPOCH_VOLUME_CAPS as string,
tokens,
decimals,
'setEpochVolumeCaps',
bridge.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.BRIDGE_LIMIT_DELAY_THRESHOLDS as string,
tokens,
decimals,
'setDelayThresholds',
bridge.setDelayThresholds,
feeOverrides
);
}
async function setOriginalTokenVaultLimits(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const originalTokenVaultAddr = process.env.ORIGINAL_TOKEN_VAULT as string;
if (!originalTokenVaultAddr) {
return;
}
const tokensStr = process.env.ORIGINAL_TOKEN_VAULT_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const originalTokenVault = OriginalTokenVault__factory.connect(originalTokenVaultAddr, deployerSigner);
const tokens = (process.env.ORIGINAL_TOKEN_VAULT_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.ORIGINAL_TOKEN_VAULT_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_MIN_DEPOSITS as string,
tokens,
decimals,
'setMinDeposit',
originalTokenVault.setMinDeposit,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_MAX_DEPOSITS as string,
tokens,
decimals,
'setMaxDeposit',
originalTokenVault.setMaxDeposit,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_EPOCH_VOLUME_CAPS as string,
tokens,
decimals,
'setEpochVolumeCaps',
originalTokenVault.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.ORIGINAL_TOKEN_VAULT_LIMIT_DELAY_THRESHOLDS as string,
tokens,
decimals,
'setDelayThresholds',
originalTokenVault.setDelayThresholds,
feeOverrides
);
}
async function setPeggedTokenBridgeLimits(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const peggedTokenBridgeAddr = process.env.PEGGED_TOKEN_BRIDGE as string;
if (!peggedTokenBridgeAddr) {
return;
}
const tokensStr = process.env.PEGGED_TOKEN_BRIDGE_LIMIT_TOKENS;
if (!tokensStr) {
return;
}
const peggedTokenBridge = PeggedTokenBridge__factory.connect(peggedTokenBridgeAddr, deployerSigner);
const tokens = (process.env.PEGGED_TOKEN_BRIDGE_LIMIT_TOKENS as string).split(',');
const decimals = (process.env.PEGGED_TOKEN_BRIDGE_LIMIT_DECIMALS as string).split(',');
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_MIN_BURNS as string,
tokens,
decimals,
'setMinBurn',
peggedTokenBridge.setMinBurn,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_MAX_BURNS as string,
tokens,
decimals,
'setMaxBurn',
peggedTokenBridge.setMaxBurn,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_EPOCH_VOLUME_CAPS as string,
tokens,
decimals,
'setEpochVolumeCaps',
peggedTokenBridge.setEpochVolumeCaps,
feeOverrides
);
await setLimitIfSpecified(
process.env.PEGGED_TOKEN_BRIDGE_LIMIT_DELAY_THRESHOLDS as string,
tokens,
decimals,
'setDelayThresholds',
peggedTokenBridge.setDelayThresholds,
feeOverrides
);
}
async function setLimits(): Promise {
await setBridgeLimits();
await setOriginalTokenVaultLimits();
await setPeggedTokenBridgeLimits();
}
setLimits();
================================================
FILE: scripts/set_msg_fee.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { deployments } from 'hardhat';
import { MessageBus__factory } from '../typechain';
import { getDeployerSigner } from './common';
dotenv.config();
async function setMsgFees(): Promise {
const deployerSigner = await getDeployerSigner();
const dep = await deployments.get('MessageBus_Proxy');
console.log('msgbus', dep.address);
const msgbus = MessageBus__factory.connect(dep.address, deployerSigner);
const owner = await msgbus.owner();
const bridge = await msgbus.liquidityBridge();
console.log('owner', owner);
console.log('bridge', bridge);
const tx0 = await msgbus.setFeeBase('1400000000000000');
tx0.wait(1);
const tx1 = await msgbus.setFeePerByte('14000000000000');
tx1.wait(10);
const feeBase = await msgbus.feeBase();
const feePerByte = await msgbus.feePerByte();
console.log(`new feeBase ${feeBase} feePerByte ${feePerByte}`);
}
setMsgFees();
================================================
FILE: scripts/solc_abigen.sh
================================================
# script for solc/abigen solidity files
# below env variables are set by github action
# PRID: ${{ github.event.number }}
# BRANCH: ${{ github.head_ref }}
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
SOLC_VER="v0.8.17+commit.8df45f5f"
OPENZEPPELIN="openzeppelin-contracts-4.5.0" # if change, also need to change the url in dld_solc
GETH_VER="geth-alltools-linux-amd64-1.10.7-12f0ff40" # for abigen
CNTRDIR="contracts" # folder name for all contracts code
GO_REPO=https://${GH_TOKEN}@github.com/celer-network/sgn-v2
# xx.sol under contracts/, no need for .sol suffix, if sol file is in subfolder, just add the relative path
solFiles=(
staking/Staking
staking/SGN
staking/StakingReward
staking/Govern
staking/Viewer
liquidity-bridge/FarmingRewards
interfaces/ISigsVerifier
)
dld_solc() {
curl -L "https://binaries.soliditylang.org/linux-amd64/solc-linux-amd64-${SOLC_VER}" -o solc && chmod +x solc
sudo mv solc /usr/local/bin/
# only need oz's contracts subfolder, files will be at $CNTRDIR/$OPENZEPPELIN/contracts
curl -L "https://github.com/OpenZeppelin/openzeppelin-contracts/archive/v4.5.0.tar.gz" | tar -xz -C $CNTRDIR $OPENZEPPELIN/contracts/
}
dld_abigen() {
curl -sL https://gethstore.blob.core.windows.net/builds/$GETH_VER.tar.gz | sudo tar -xz -C /usr/local/bin --strip 1 $GETH_VER/abigen
sudo chmod +x /usr/local/bin/abigen
}
# MUST run this under $CNTRDIR
gen_dtHelper() {
pushd libraries
DTFILE="staking/DataTypes.sol"
CTRNAME="DtHelper"
SOLFILE="$CTRNAME.sol"
cat >$SOLFILE <=0.8.0;
pragma abicoder v2;
import {DataTypes as dt} from "./$DTFILE";
contract $CTRNAME {
EOF
grep -Eo "struct [^ ]*" $DTFILE | cut -d' ' -f2 | while read STRUCT; do
echo " function $STRUCT(dt.$STRUCT calldata _in) public pure {}" >>$SOLFILE
done
echo "}" >>$SOLFILE
popd
}
# MUST run this under repo root
# will generate a single combined.json under $CNTRDIR
run_solc() {
pushd $CNTRDIR
gen_dtHelper
solc --base-path $PWD --allow-paths . --overwrite --optimize --optimize-runs 800 --pretty-json --combined-json abi,bin -o . '@openzeppelin/'=$OPENZEPPELIN/ \
$(for f in ${solFiles[@]}; do echo -n "$f.sol "; done)
no_openzeppelin combined.json # combined.json file name is hardcoded in solc
popd
}
# remove openzeppelin from combined.json. solc will also include all openzeppelin in combined.json but we don't want to generate go for them
# $1 is the json file from solc output
no_openzeppelin() {
jq '."contracts"|=with_entries(select(.key|test("^openzeppelin")|not))' $1 >tmp.json
mv tmp.json $1
}
# MUST run this under contract repo root
run_abigen() {
PR_COMMIT_ID=$(git rev-parse --short HEAD)
git clone $GO_REPO
pushd sgn-v2
git fetch
BR="$BRANCH-binding"
git checkout $BR || git checkout -b $BR
mkdir -p eth
abigen -combined-json ../$CNTRDIR/combined.json -pkg eth -out eth/bindings.go
#pushd eth
#go build # make sure eth pkg can build
#popd
if [[ $(git status --porcelain) ]]; then
echo "Sync-ing go binding"
git add .
git commit -m "Sync go binding based on contract PR $PRID" -m "contract repo commit: $PR_COMMIT_ID"
git push origin $BR
fi
popd
}
setup_git() {
git config --global user.email "build@celer.network"
git config --global user.name "Build Bot"
git config --global push.default "current"
}
================================================
FILE: scripts/solt.sh
================================================
#!/bin/sh
# Script to run solt and generate standard-json files for Etherscan verification.
solFiles=(
circle-usdc/CircleBridgeProxy
governed-owner/GovernedOwnerProxy
governed-owner/SimpleGovernance
integration-examples/ContractAsLP
integration-examples/ContractAsSender
interfaces/ISigsVerifier
liquidity-bridge/Bridge
liquidity-bridge/FarmingRewards
liquidity-bridge/WithdrawInbox
message/apps/TransferSwap
message/apps/OrigNFT
message/apps/PegNFT
message/apps/MCNNFT
message/apps/NFTBridge
message/apps/MsgTest
message/apps/RFQ
message/apps/adapter/MessageReceiverAdapter
message/messagebus/MessageBus
miscs/Faucet
miscs/MintableERC20
pegged-bridge/OriginalTokenVault
pegged-bridge/OriginalTokenVaultV2
pegged-bridge/PeggedTokenBridge
pegged-bridge/PeggedTokenBridgeV2
pegged-bridge/tokens/ERC20Permit/MintSwapCanonicalTokenPermit
pegged-bridge/tokens/ERC20Permit/MultiBridgeTokenPermit
pegged-bridge/tokens/ERC20Permit/SingleBridgeTokenPermit
pegged-bridge/tokens/MintSwapCanonicalToken
pegged-bridge/tokens/MultiBridgeToken
pegged-bridge/tokens/SingleBridgeToken
pegged-bridge/tokens/SwapBridgeToken
pegged-bridge/tokens/customized/FraxBridgeToken
pegged-bridge/tokens/customized/CircleBridgeToken
pegged-bridge/tokens/owners/RestrictedMultiBridgeTokenOwner
pegged-bridge/customized/PeggedNativeTokenBridge
proxy/TransferAgent
staking/Govern
staking/SGN
staking/Staking
staking/StakingReward
staking/Viewer
test-helpers/DummySwap
test-helpers/WETH
)
run_solt_write() {
for f in ${solFiles[@]}; do
solt write contracts/$f.sol --npm --runs 800
done
}
================================================
FILE: scripts/transfer_ownership.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { Ownable__factory } from '../typechain';
import { getDeployerSigner, getFeeOverrides } from './common';
dotenv.config();
async function transferOwnership(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const contractAddr = process.env.TRANSFER_OWNERSHIP_CONTRACT as string;
if (!contractAddr) {
return;
}
const newOwner = process.env.TRANSFER_OWNERSHIP_NEW_OWNER as string;
if (!newOwner) {
return;
}
const contract = Ownable__factory.connect(contractAddr, deployerSigner);
await (await contract.transferOwnership(newOwner, feeOverrides)).wait();
console.log('transferOwnership', contractAddr, newOwner);
}
transferOwnership();
================================================
FILE: scripts/update_bridge_supply_cap.ts
================================================
import 'hardhat-deploy';
import * as dotenv from 'dotenv';
import { MintSwapCanonicalToken__factory } from '../typechain';
import { getDeployerSigner, getFeeOverrides } from './common';
dotenv.config();
async function updateBridgeSupplyCap(): Promise {
const deployerSigner = await getDeployerSigner();
const feeOverrides = await getFeeOverrides();
const tokenAddr = process.env.MINT_SWAP_CANONICAL_TOKEN as string;
if (!tokenAddr) {
return;
}
const bridgeAddr = process.env.MINT_SWAP_CANONICAL_TOKEN_BRIDGE as string;
if (!bridgeAddr) {
return;
}
const mintSwapCanonicalToken = MintSwapCanonicalToken__factory.connect(tokenAddr, deployerSigner);
const cap = process.env.MINT_SWAP_CANONICAL_TOKEN_CAP as string;
await (await mintSwapCanonicalToken.updateBridgeSupplyCap(bridgeAddr, cap, feeOverrides)).wait();
console.log('updateBridgeSupplyCap', tokenAddr, bridgeAddr, cap);
}
updateBridgeSupplyCap();
================================================
FILE: test/Basics.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, solidityPackedKeccak256, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { SGN, Staking, TestERC20, Viewer } from '../typechain';
import { advanceBlockNumber, deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
describe('Basic Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, sgn, viewer, celr } = await deployContracts(admin);
return { admin, staking, sgn, viewer, celr };
}
const abiCoder = AbiCoder.defaultAbiCoder();
let staking: Staking;
let sgn: SGN;
let viewer: Viewer;
let celr: TestERC20;
let admin: HardhatEthersSigner;
let validator: Wallet;
let delegator: Wallet;
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
sgn = res.sgn;
viewer = res.viewer;
celr = res.celr;
admin = res.admin;
const accounts = await getAccounts(res.admin, [celr], 2);
validator = accounts[0];
delegator = accounts[1];
const stakingAddress = await staking.getAddress();
await celr.connect(validator).approve(stakingAddress, parseUnits('100'));
await celr.connect(delegator).approve(stakingAddress, parseUnits('100'));
});
it('should fail to delegate to an uninitialized validator', async function () {
await expect(staking.delegate(validator.address, consts.DELEGATOR_STAKE)).to.be.revertedWith(
'Validator is not initialized'
);
});
it('should fail to initialize a validator when paused', async function () {
await staking.pause();
await expect(
staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
).to.be.revertedWith('Pausable: paused');
});
it('should fail to initialize a validator with insufficient min self delegation', async function () {
await expect(
staking.connect(validator).initializeValidator(validator.address, parseUnits('1'), consts.COMMISSION_RATE)
).to.be.revertedWith('Insufficient min self delegation');
});
it('should fail to initialize a validator if self delegation fails', async function () {
await expect(
staking.initializeValidator(admin.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
).to.be.revertedWith('ERC20: insufficient allowance');
});
it('should fail to initialize a non-whitelisted validator when whitelist is enabled', async function () {
await staking.setWhitelistEnabled(true);
await expect(
staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
).to.be.revertedWith('Caller is not whitelisted');
});
it('should initialize a whitelisted validator successfully when whitelist is enabled', async function () {
await staking.setWhitelistEnabled(true);
await staking.addWhitelisted(validator.address);
const data = abiCoder.encode(
['address', 'uint256', 'uint256'],
[validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE]
);
await expect(
staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
)
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'init', data, ZeroAddress);
});
it('should initialize a validator and update sgn address successfully', async function () {
const data = abiCoder.encode(
['address', 'uint256', 'uint256'],
[validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE]
);
await expect(
staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
)
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'init', data, ZeroAddress);
const sgnAddr = solidityPackedKeccak256(['string'], ['sgnaddr1']);
await expect(sgn.connect(validator).updateSgnAddr(sgnAddr))
.to.emit(sgn, 'SgnAddrUpdate')
.withArgs(validator.address, '0x', sgnAddr)
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'sgn-addr', sgnAddr, sgn.getAddress());
});
describe('after one validator finishes initialization', async () => {
const sgnAddr = solidityPackedKeccak256(['string'], ['sgnaddr']);
beforeEach(async () => {
await staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
await sgn.connect(validator).updateSgnAddr(sgnAddr);
});
it('should fail to initialize the same validator twice', async function () {
await expect(
staking
.connect(validator)
.initializeValidator(validator.address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE)
).to.be.revertedWith('Validator is initialized');
});
it('should pass sgn address update', async function () {
const newSgnAddr = solidityPackedKeccak256(['string'], ['sgnaddr_new']);
await expect(sgn.connect(validator).updateSgnAddr(newSgnAddr))
.to.emit(sgn, 'SgnAddrUpdate')
.withArgs(validator.address, sgnAddr, newSgnAddr)
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'sgn-addr', newSgnAddr, sgn.getAddress());
await expect(sgn.connect(admin).updateSgnAddr(newSgnAddr)).to.be.revertedWith('Not unbonded validator');
});
it('should fail to delegate when paused', async function () {
await staking.pause();
await expect(staking.delegate(validator.address, consts.DELEGATOR_STAKE)).to.be.revertedWith('Pausable: paused');
});
it('should delegate to validator by a delegator successfully', async function () {
await expect(staking.connect(delegator).delegate(validator.address, consts.DELEGATOR_STAKE))
.to.emit(staking, 'DelegationUpdate')
.withArgs(
validator.address,
delegator.address,
consts.DELEGATOR_STAKE + consts.MIN_SELF_DELEGATION,
consts.DELEGATOR_STAKE,
consts.DELEGATOR_STAKE
);
});
it('should fail to bondValidator before delegating enough stake', async function () {
const shouldBond = await viewer.shouldBondValidator(validator.address);
expect(shouldBond).to.equal(false);
await expect(staking.connect(validator).bondValidator()).to.be.revertedWith('Not have min tokens');
});
describe('after one delegator delegates enough stake to the validator', async () => {
beforeEach(async () => {
await staking.connect(delegator).delegate(validator.address, consts.DELEGATOR_STAKE);
});
it('should fail to bondValidator before self delegating minSelfDelegation', async function () {
await staking.connect(validator).undelegateShares(validator.address, parseUnits('1'));
await expect(staking.connect(validator).bondValidator()).to.be.revertedWith('Not have min tokens');
});
it('should undelegate from unbonded validator by delegator successfully', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, consts.DELEGATOR_STAKE))
.to.emit(staking, 'Undelegated')
.withArgs(validator.address, delegator.address, consts.DELEGATOR_STAKE);
});
it('should fail to undelegate from unbonded validator more than it delegated', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, consts.DELEGATOR_STAKE + 1000n)).to
.be.reverted;
});
it('should fail to undelegate from unbonded validator with amount smaller than 1 share', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, 1000)).to.be.revertedWith(
'Minimal amount is 1 share'
);
});
it('should fail to drain token when not paused', async function () {
await expect(staking.drainToken(consts.DELEGATOR_STAKE)).to.be.revertedWith('Pausable: not paused');
});
it('should drainToken successfully when paused', async function () {
await staking.pause();
const balanceBefore = await celr.balanceOf(admin.address);
await staking.drainToken(consts.DELEGATOR_STAKE);
const balanceAfter = await celr.balanceOf(admin.address);
expect(balanceAfter - balanceBefore).to.equal(consts.DELEGATOR_STAKE);
});
describe('after one delegator delegates enough stake to the validator', async () => {
beforeEach(async () => {
await staking.connect(validator).delegate(validator.address, consts.VALIDATOR_STAKE);
});
it('should bondValidator successfully', async function () {
const shouldBond = await viewer.shouldBondValidator(validator.address);
expect(shouldBond).to.equal(true);
await expect(staking.connect(validator).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validator.address, consts.STATUS_BONDED);
});
it('should increase min self delegation and bondValidator successfully', async function () {
const higherMinSelfDelegation = consts.MIN_SELF_DELEGATION + 1000000n;
const data = abiCoder.encode(['uint256'], [higherMinSelfDelegation]);
await expect(staking.connect(validator).updateMinSelfDelegation(higherMinSelfDelegation))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'min-self-delegation', data, ZeroAddress);
await expect(staking.connect(validator).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validator.address, consts.STATUS_BONDED);
});
it('should decrease min self delegation and only able to bondValidator after notice period', async function () {
let minSelfDelegation = consts.MIN_SELF_DELEGATION + 1000000n;
await staking.connect(validator).updateMinSelfDelegation(minSelfDelegation);
minSelfDelegation = consts.MIN_SELF_DELEGATION + 10n;
const data = abiCoder.encode(['uint256'], [minSelfDelegation]);
await expect(staking.connect(validator).updateMinSelfDelegation(minSelfDelegation))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'min-self-delegation', data, ZeroAddress);
await expect(staking.connect(validator).bondValidator()).to.be.revertedWith('Bond block not reached');
await advanceBlockNumber(consts.ADVANCE_NOTICE_PERIOD);
await expect(staking.connect(validator).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validator.address, consts.STATUS_BONDED);
});
describe('after one validator bondValidator', async () => {
beforeEach(async () => {
await staking.connect(validator).bondValidator();
});
it('should fail to undelegate with amount smaller than 1 share', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, 1000)).to.be.revertedWith(
'Minimal amount is 1 share'
);
});
it('should fail to undelegate more than it delegated', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, consts.DELEGATOR_STAKE + 1000n))
.to.be.reverted;
});
it('should remove the validator after validator undelegate to become under minSelfDelegation', async function () {
await expect(staking.connect(validator).undelegateShares(validator.address, consts.MIN_SELF_DELEGATION))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validator.address, consts.STATUS_UNBONDING);
});
it('should remove the validator after delegator undelegate to become under minStakingPool', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, consts.DELEGATOR_STAKE))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validator.address, consts.STATUS_UNBONDING)
.to.emit(staking, 'DelegationUpdate')
.withArgs(
validator.address,
delegator.address,
consts.VALIDATOR_STAKE + consts.MIN_SELF_DELEGATION,
0,
parseUnits('0') - consts.DELEGATOR_STAKE
);
});
it('should pass min self delegation updates', async function () {
let minSelfDelegation = consts.MIN_SELF_DELEGATION + 1000000n;
const data = abiCoder.encode(['uint256'], [minSelfDelegation]);
await expect(staking.connect(validator).updateMinSelfDelegation(minSelfDelegation))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validator.address, 'min-self-delegation', data, ZeroAddress);
minSelfDelegation = consts.MIN_SELF_DELEGATION + 100n;
await expect(staking.connect(validator).updateMinSelfDelegation(minSelfDelegation)).to.be.revertedWith(
'Validator is bonded'
);
minSelfDelegation = consts.MIN_SELF_DELEGATION - 100n;
await expect(staking.connect(validator).updateMinSelfDelegation(minSelfDelegation)).to.be.revertedWith(
'Insufficient min self delegation'
);
});
describe('after a delegator undelegate', async () => {
beforeEach(async () => {
await staking.connect(delegator).undelegateShares(validator.address, parseUnits('2'));
});
it('should fail to undelegate with a total more than it delegated', async function () {
await expect(staking.connect(delegator).undelegateShares(validator.address, consts.DELEGATOR_STAKE)).to.be
.reverted;
});
it('should completeUndelegate successfully', async function () {
// before withdrawTimeout
await expect(staking.connect(delegator).completeUndelegate(validator.address)).to.be.revertedWith(
'No undelegation ready to be completed'
);
// after withdrawTimeout
await advanceBlockNumber(consts.UNBONDING_PERIOD);
// first completeUndelegate
await expect(staking.connect(delegator).completeUndelegate(validator.address))
.to.emit(staking, 'Undelegated')
.withArgs(validator.address, delegator.address, parseUnits('2'));
// second completeUndelegate
await expect(staking.connect(delegator).completeUndelegate(validator.address)).to.be.revertedWith(
'No undelegation ready to be completed'
);
});
it('should pass with multiple undelegations', async function () {
await staking.connect(delegator).undelegateShares(validator.address, parseUnits('1'));
let res = await staking.getDelegatorInfo(validator.address, delegator.address);
expect(res.shares).to.equal(parseUnits('3'));
expect(res.undelegations[0].shares).to.equal(parseUnits('2'));
expect(res.undelegations[1].shares).to.equal(parseUnits('1'));
await advanceBlockNumber(consts.UNBONDING_PERIOD);
await staking.connect(delegator).undelegateShares(validator.address, parseUnits('1'));
await expect(staking.connect(delegator).completeUndelegate(validator.address))
.to.emit(staking, 'Undelegated')
.withArgs(validator.address, delegator.address, parseUnits('3'));
res = await staking.getDelegatorInfo(validator.address, delegator.address);
expect(res.shares).to.equal(parseUnits('2'));
expect(res.undelegations[0].shares).to.equal(parseUnits('1'));
await expect(staking.connect(delegator).completeUndelegate(validator.address)).to.be.revertedWith(
'No undelegation ready to be completed'
);
await advanceBlockNumber(consts.UNBONDING_PERIOD);
await expect(staking.connect(delegator).completeUndelegate(validator.address))
.to.emit(staking, 'Undelegated')
.withArgs(validator.address, delegator.address, parseUnits('1'));
res = await staking.getDelegatorInfo(validator.address, delegator.address);
expect(res.shares).to.equal(parseUnits('2'));
expect(res.undelegations.length).to.equal(0);
});
});
});
});
});
});
});
================================================
FILE: test/Bridge.spec.ts
================================================
import { expect } from 'chai';
import { getBytes, parseUnits, solidityPacked, solidityPackedKeccak256, toNumber, Wallet } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Bridge, PeggedTokenBridge, SingleBridgeToken, TestERC20 } from '../typechain';
import { deployBridgeContracts, getAccounts, getAddrs, getBlockTime } from './lib/common';
import { calculateSignatures, getMintRequest, getRelayRequest, getWithdrawRequest } from './lib/proto';
async function getUpdateSignersSigs(
triggerTime: number,
newSignerAddrs: string[],
newPowers: bigint[],
currSigners: Wallet[],
chainId: number,
contractAddress: string
) {
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, contractAddress, 'UpdateSigners']);
const data = solidityPacked(
['bytes32', 'uint256', 'address[]', 'uint256[]'],
[domain, triggerTime, newSignerAddrs, newPowers]
);
const hash = solidityPackedKeccak256(['bytes'], [data]);
const sigs = await calculateSignatures(currSigners, getBytes(hash));
return sigs;
}
describe('Bridge Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { bridge, token, pegBridge, pegToken } = await deployBridgeContracts(admin);
return { admin, bridge, token, pegBridge, pegToken };
}
let bridge: Bridge;
let token: TestERC20;
let pegBridge: PeggedTokenBridge;
let pegToken: SingleBridgeToken;
let accounts: Wallet[];
let chainId: number;
beforeEach(async () => {
const res = await loadFixture(fixture);
bridge = res.bridge;
token = res.token;
pegBridge = res.pegBridge;
pegToken = res.pegToken;
accounts = await getAccounts(res.admin, [token], 4);
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
});
it('should update signers correctly', async function () {
await expect(bridge.resetSigners([accounts[0].address], [parseUnits('1')]))
.to.emit(bridge, 'SignersUpdated')
.withArgs([accounts[0].address], [parseUnits('1')]);
let newSigners = [accounts[2], accounts[1], accounts[0], accounts[3]];
let newPowers = [parseUnits('12'), parseUnits('11'), parseUnits('10'), parseUnits('9')];
let triggerTime = 1;
let sigs = await getUpdateSignersSigs(
triggerTime,
getAddrs(newSigners),
newPowers,
[accounts[0]],
chainId,
await bridge.getAddress()
);
await expect(
bridge.updateSigners(triggerTime, getAddrs(newSigners), newPowers, sigs, [accounts[0].address], [parseUnits('1')])
).to.be.revertedWith('New signers not in ascending order');
newSigners = [accounts[0], accounts[1], accounts[2], accounts[3]];
triggerTime = 2;
sigs = await getUpdateSignersSigs(
triggerTime,
getAddrs(newSigners),
newPowers,
[accounts[0]],
chainId,
await bridge.getAddress()
);
await expect(
bridge.updateSigners(triggerTime, getAddrs(newSigners), newPowers, sigs, [accounts[0].address], [parseUnits('1')])
)
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(newSigners), newPowers);
await expect(
bridge.updateSigners(
triggerTime,
getAddrs(newSigners),
newPowers,
sigs,
[accounts[0].address],
[parseUnits('10')]
)
).to.be.revertedWith('Trigger time is not increasing');
await expect(
bridge.updateSigners(
parseUnits('1'),
getAddrs(newSigners),
newPowers,
sigs,
[accounts[0].address],
[parseUnits('10')]
)
).to.be.revertedWith('Trigger time is too large');
triggerTime = 3;
await expect(
bridge.updateSigners(
triggerTime,
getAddrs(newSigners),
newPowers,
sigs,
[accounts[0].address],
[parseUnits('10')]
)
).to.be.revertedWith('Mismatch current signers');
const curSigners = newSigners;
const curPowers = newPowers;
newSigners = [accounts[1], accounts[3]];
newPowers = [parseUnits('15'), parseUnits('50')];
sigs = await getUpdateSignersSigs(
triggerTime,
getAddrs(newSigners),
newPowers,
curSigners,
chainId,
await bridge.getAddress()
);
await expect(
bridge.updateSigners(
triggerTime,
getAddrs(newSigners),
newPowers,
[sigs[0], sigs[1]],
getAddrs(curSigners),
curPowers
)
).to.be.revertedWith('quorum not reached');
await expect(
bridge.updateSigners(
triggerTime,
getAddrs(newSigners),
newPowers,
[sigs[1], sigs[0]],
getAddrs(curSigners),
curPowers
)
).to.be.revertedWith('signers not in ascending order');
await expect(
bridge.updateSigners(
triggerTime,
getAddrs(newSigners),
newPowers,
[sigs[0], sigs[1], sigs[2]],
getAddrs(curSigners),
curPowers
)
)
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(newSigners), newPowers);
});
it('should send and relay successfully', async function () {
await bridge.setDelayThresholds([await token.getAddress()], [parseUnits('5')]);
await bridge.setDelayPeriod(10);
const signers = [accounts[0], accounts[1], accounts[2]];
const powers = [parseUnits('10'), parseUnits('10'), parseUnits('10')];
await expect(bridge.resetSigners(getAddrs(signers), powers))
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(signers), powers);
const sender = accounts[0];
const receiver = accounts[1];
const amount = parseUnits('1');
const nonce = 0;
const slippage = 1000;
const srcXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'uint64'],
[sender.address, receiver.address, await token.getAddress(), amount, chainId, nonce, chainId]
);
await token.connect(sender).approve(await bridge.getAddress(), parseUnits('100'));
await bridge.connect(sender).addLiquidity(await token.getAddress(), parseUnits('50'));
await expect(
bridge.connect(sender).send(receiver.address, await token.getAddress(), amount, chainId, nonce, slippage)
)
.to.emit(bridge, 'Send')
.withArgs(
srcXferId,
sender.address,
receiver.address,
await token.getAddress(),
amount,
chainId,
nonce,
slippage
);
let req = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
amount,
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
let dstXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender.address, receiver.address, await token.getAddress(), amount, chainId, chainId, srcXferId]
);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'Relay')
.withArgs(dstXferId, sender.address, receiver.address, await token.getAddress(), amount, chainId, srcXferId);
const largeAmount = parseUnits('10');
req = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
largeAmount,
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
dstXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender.address, receiver.address, await token.getAddress(), largeAmount, chainId, chainId, srcXferId]
);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'Relay')
.withArgs(dstXferId, sender.address, receiver.address, await token.getAddress(), largeAmount, chainId, srcXferId)
.to.emit(bridge, 'DelayedTransferAdded')
.withArgs(dstXferId);
await expect(bridge.executeDelayedTransfer(dstXferId)).to.be.revertedWith('delayed transfer still locked');
await ethers.provider.send('evm_increaseTime', [100]);
await ethers.provider.send('evm_mine', []);
await expect(bridge.executeDelayedTransfer(dstXferId))
.to.emit(bridge, 'DelayedTransferExecuted')
.withArgs(dstXferId, receiver.address, await token.getAddress(), largeAmount);
await expect(bridge.executeDelayedTransfer(dstXferId)).to.be.revertedWith('delayed transfer not exist');
});
it('should pass risk control tests', async function () {
const signers = [accounts[0], accounts[1], accounts[2]];
const powers = [parseUnits('10'), parseUnits('10'), parseUnits('10')];
await expect(bridge.resetSigners(getAddrs(signers), powers))
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(signers), powers);
const epochLength = 20;
await bridge.setEpochLength(epochLength);
await bridge.setEpochVolumeCaps([await token.getAddress()], [parseUnits('5')]);
await bridge.setMaxSend([await token.getAddress()], [parseUnits('5')]);
const sender = accounts[0];
const receiver = accounts[1];
await token.connect(sender).approve(await bridge.getAddress(), parseUnits('100'));
await expect(
bridge.connect(sender).send(receiver.address, await token.getAddress(), parseUnits('10'), chainId, 0, 10000)
).to.be.revertedWith('amount too large');
await bridge.setMinAdd([await token.getAddress()], [parseUnits('10')]);
await expect(bridge.connect(sender).addLiquidity(await token.getAddress(), parseUnits('50')))
.to.emit(bridge, 'LiquidityAdded')
.withArgs(1, sender.address, await token.getAddress(), parseUnits('50'));
await expect(bridge.connect(sender).addLiquidity(await token.getAddress(), parseUnits('5'))).to.be.revertedWith(
'amount too small'
);
const srcXferId = solidityPackedKeccak256(['string'], ['srcId']);
let dstXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender.address, receiver.address, await token.getAddress(), parseUnits('2'), chainId, chainId, srcXferId]
);
let req = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('2'),
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
let blockTime = await getBlockTime();
let epochStartTime = Math.floor(blockTime / epochLength) * epochLength;
await ethers.provider.send('evm_setNextBlockTimestamp', [epochStartTime + epochLength]);
await ethers.provider.send('evm_mine', []);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'Relay')
.withArgs(
dstXferId,
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('2'),
chainId,
srcXferId
);
req = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('4'),
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
dstXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender.address, receiver.address, await token.getAddress(), parseUnits('4'), chainId, chainId, srcXferId]
);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers)).to.be.revertedWith(
'volume exceeds cap'
);
blockTime = await getBlockTime();
epochStartTime = Math.floor(blockTime / epochLength) * epochLength;
await ethers.provider.send('evm_setNextBlockTimestamp', [epochStartTime + epochLength]);
await ethers.provider.send('evm_mine', []);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'Relay')
.withArgs(
dstXferId,
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('4'),
chainId,
srcXferId
);
req = await getRelayRequest(
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('3'),
chainId,
chainId,
srcXferId,
signers,
await bridge.getAddress()
);
dstXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender.address, receiver.address, await token.getAddress(), parseUnits('3'), chainId, chainId, srcXferId]
);
for (let i = 0; i < 3; i++) {
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers)).to.be.revertedWith(
'volume exceeds cap'
);
}
blockTime = await getBlockTime();
epochStartTime = Math.floor(blockTime / epochLength) * epochLength;
await ethers.provider.send('evm_setNextBlockTimestamp', [epochStartTime + epochLength]);
await ethers.provider.send('evm_mine', []);
await expect(bridge.relay(req.relayBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'Relay')
.withArgs(
dstXferId,
sender.address,
receiver.address,
await token.getAddress(),
parseUnits('3'),
chainId,
srcXferId
);
});
it('should withdraw liquidity correctly', async function () {
const signers = [accounts[0], accounts[1], accounts[2]];
const powers = [parseUnits('10'), parseUnits('10'), parseUnits('10')];
await expect(bridge.resetSigners(getAddrs(signers), powers))
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(signers), powers);
const account = accounts[0];
await token.connect(account).approve(await bridge.getAddress(), parseUnits('100'));
await bridge.connect(account).addLiquidity(await token.getAddress(), parseUnits('50'));
const refId = solidityPackedKeccak256(['string'], ['random']);
const seqnum = 1;
const amount = parseUnits('10');
const req = await getWithdrawRequest(
chainId,
seqnum,
account.address,
await token.getAddress(),
amount,
refId,
signers,
await bridge.getAddress()
);
const wdId = solidityPackedKeccak256(
['uint64', 'uint64', 'address', 'address', 'uint256'],
[chainId, seqnum, account.address, await token.getAddress(), amount]
);
await expect(bridge.withdraw(req.withdrawBytes, req.sigs, getAddrs(signers), powers))
.to.emit(bridge, 'WithdrawDone')
.withArgs(wdId, seqnum, account.address, await token.getAddress(), amount, refId);
});
it('should mint successfully', async function () {
const signers = [accounts[0], accounts[1], accounts[2]];
const powers = [parseUnits('10'), parseUnits('10'), parseUnits('10')];
await expect(bridge.resetSigners(getAddrs(signers), powers))
.to.emit(bridge, 'SignersUpdated')
.withArgs(getAddrs(signers), powers);
const account = accounts[0];
const amount = parseUnits('10');
const refChainId = 101;
const refId = solidityPackedKeccak256(['string'], ['random']);
const req = await getMintRequest(
await pegToken.getAddress(),
account.address,
amount,
account.address,
refChainId,
refId,
signers,
chainId,
await pegBridge.getAddress()
);
const mintId = solidityPackedKeccak256(
['address', 'address', 'uint256', 'address', 'uint64', 'bytes32'],
[account.address, await pegToken.getAddress(), amount, account.address, refChainId, refId]
);
await expect(pegBridge.mint(req.mintBytes, req.sigs, getAddrs(signers), powers))
.to.emit(pegBridge, 'Mint')
.withArgs(mintId, await pegToken.getAddress(), account.address, amount, refChainId, refId, account.address);
});
});
================================================
FILE: test/FarmingRewards.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, toNumber, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { FarmingRewards, Staking, TestERC20 } from '../typechain';
import { deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
import { getFarmingRewardsRequest } from './lib/proto';
describe('FarmingRewards Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, farmingRewards, celr } = await deployContracts(admin);
return { admin, staking, farmingRewards, celr };
}
const abiCoder = AbiCoder.defaultAbiCoder();
let staking: Staking;
let rewards: FarmingRewards;
let celr: TestERC20;
let validators: Wallet[];
let signers: Wallet[];
let chainId: number;
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
rewards = res.farmingRewards;
celr = res.celr;
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
const accounts = await getAccounts(res.admin, [celr], 6);
validators = [accounts[0], accounts[1], accounts[2], accounts[3]];
signers = [accounts[0], accounts[1], accounts[4], accounts[5]];
for (let i = 0; i < 4; i++) {
await celr.connect(validators[i]).approve(staking.getAddress(), parseUnits('100'));
await celr.connect(validators[i]).approve(rewards.getAddress(), parseUnits('100'));
await staking
.connect(validators[i])
.initializeValidator(signers[i].address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
await staking.connect(validators[i]).delegate(validators[i].address, consts.MIN_VALIDATOR_TOKENS);
await staking.connect(validators[i]).bondValidator();
}
await rewards.connect(validators[0]).contributeToRewardPool(celr.getAddress(), 100);
});
it('should fail to contribute to reward pool when paused', async function () {
await rewards.pause();
await expect(rewards.contributeToRewardPool(celr.getAddress(), 100)).to.be.revertedWith('Pausable: paused');
});
it('should contribute to reward pool successfully', async function () {
await expect(rewards.connect(validators[0]).contributeToRewardPool(celr.getAddress(), 100))
.to.emit(rewards, 'FarmingRewardContributed')
.withArgs(validators[0].address, celr.getAddress(), 100);
});
it('should update the commission rate lock successfully', async function () {
const newRate = consts.COMMISSION_RATE + 10;
const data = abiCoder.encode(['uint256'], [newRate]);
await expect(staking.connect(validators[0]).updateCommissionRate(newRate))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validators[0].address, 'commission', data, ZeroAddress);
});
it('should fail to claim reward when paused', async function () {
await rewards.pause();
const r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('100', 'wei')],
signers,
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], [])).to.be.revertedWith('Pausable: paused');
});
it('should claim reward successfully', async function () {
let r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('40', 'wei')],
signers,
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], []))
.to.emit(rewards, 'FarmingRewardClaimed')
.withArgs(validators[0].address, await celr.getAddress(), 40);
r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('90', 'wei')],
signers,
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], []))
.to.emit(rewards, 'FarmingRewardClaimed')
.withArgs(validators[0].address, await celr.getAddress(), 50);
});
it('should fail to claim reward more than amount in reward pool', async function () {
const r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('101', 'wei')],
signers,
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], [])).to.be.revertedWith(
'ERC20: transfer amount exceeds balance'
);
});
it('should fail to claim reward if there is no new reward', async function () {
const r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('0')],
signers,
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], [])).to.be.revertedWith('No new reward');
});
it('should fail to claim reward with insufficient signatures', async function () {
const r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('10', 'wei')],
[signers[0], signers[1]],
chainId,
await rewards.getAddress()
);
await expect(rewards.claimRewards(r.rewardBytes, r.sigs, [], [])).to.be.revertedWith('Quorum not reached');
});
it('should fail to claim reward with disordered signatures', async function () {
const r = await getFarmingRewardsRequest(
validators[0].address,
[await celr.getAddress()],
[parseUnits('10', 'wei')],
signers,
chainId,
await rewards.getAddress()
);
await expect(
rewards.claimRewards(r.rewardBytes, [r.sigs[0], r.sigs[2], r.sigs[1], r.sigs[3]], [], [])
).to.be.revertedWith('Signers not in ascending order');
await expect(
rewards.claimRewards(r.rewardBytes, [r.sigs[0], r.sigs[0], r.sigs[1], r.sigs[2]], [], [])
).to.be.revertedWith('Signers not in ascending order');
});
});
================================================
FILE: test/Governance.spec.ts
================================================
import { expect } from 'chai';
import { parseUnits, Wallet } from 'ethers';
import { ethers } from 'hardhat';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Govern, Staking, TestERC20 } from '../typechain';
import { advanceBlockNumber, deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
describe('Governance Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, govern, celr } = await deployContracts(admin);
return { admin, staking, govern, celr };
}
let staking: Staking;
let govern: Govern;
let celr: TestERC20;
let admin: HardhatEthersSigner;
let validators: Wallet[];
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
govern = res.govern;
celr = res.celr;
admin = res.admin;
const stakingAddress = await staking.getAddress();
const governAddress = await govern.getAddress();
await staking.setGovContract(governAddress);
validators = await getAccounts(res.admin, [celr], 4);
for (let i = 0; i < 4; i++) {
await celr.connect(validators[i]).approve(stakingAddress, parseUnits('100'));
await celr.connect(validators[i]).approve(governAddress, parseUnits('100'));
await staking
.connect(validators[i])
.initializeValidator(validators[i].address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
await staking.connect(validators[i]).delegate(validators[i].address, parseUnits('6'));
await staking.connect(validators[i]).bondValidator();
}
await celr.approve(stakingAddress, parseUnits('100'));
await celr.approve(governAddress, parseUnits('100'));
});
it('should createParamProposal successfully', async function () {
const newUnbondingPeriod = consts.UNBONDING_PERIOD + 1;
const blockNumber = await ethers.provider.getBlockNumber();
await expect(govern.createParamProposal(consts.ENUM_UNBONDING_PERIOD, newUnbondingPeriod))
.to.emit(govern, 'CreateParamProposal')
.withArgs(
0,
admin.address,
consts.PROPOSAL_DEPOSIT,
consts.VOTING_PERIOD + blockNumber + 1,
consts.ENUM_UNBONDING_PERIOD,
newUnbondingPeriod
);
});
describe('after createParamProposal successfully', async () => {
const proposalId = 0;
const paramType = consts.ENUM_MAX_VALIDATOR_NUM;
const paramValue = 25;
beforeEach(async () => {
await govern.createParamProposal(paramType, paramValue);
});
it('should fail to voteParam if not validator', async function () {
await expect(govern.voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES)).to.be.revertedWith(
'Voter is not a bonded validator'
);
});
it('should fail to voteParam for a proposal with an invalid status', async function () {
await expect(
govern.connect(validators[0]).voteParam(proposalId + 1, consts.ENUM_VOTE_OPTION_YES)
).to.be.revertedWith('Invalid proposal status');
});
it('should vote successfully as a validator', async function () {
await expect(govern.connect(validators[0]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES))
.to.emit(govern, 'VoteParam')
.withArgs(proposalId, validators[0].address, consts.ENUM_VOTE_OPTION_YES);
});
describe('after a validator votes successfully', async () => {
beforeEach(async () => {
await govern.connect(validators[0]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES);
});
it('should fail to vote for the same proposal twice', async function () {
await expect(
govern.connect(validators[0]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES)
).to.be.revertedWith('Voter has voted');
});
it('should fail to confirmParamProposal before the vote deadline', async function () {
await expect(govern.confirmParamProposal(proposalId)).to.be.revertedWith('Vote deadline not reached');
});
it('should accept proposal after over 2/3 voted for Yes', async function () {
await expect(govern.connect(validators[1]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES))
.to.emit(govern, 'VoteParam')
.withArgs(proposalId, validators[1].address, consts.ENUM_VOTE_OPTION_YES);
await govern.connect(validators[2]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES);
await advanceBlockNumber(consts.VOTING_PERIOD);
await expect(govern.confirmParamProposal(proposalId))
.to.emit(govern, 'ConfirmParamProposal')
.withArgs(proposalId, true, paramType, paramValue);
const val = await staking.params(consts.ENUM_MAX_VALIDATOR_NUM);
expect(val).to.equal(paramValue);
});
describe('after passing the vote deadline with less than 2/3 votes', async () => {
beforeEach(async () => {
await advanceBlockNumber(consts.VOTING_PERIOD);
});
it('should fail to vote after the vote deadline', async function () {
await expect(
govern.connect(validators[2]).voteParam(proposalId, consts.ENUM_VOTE_OPTION_YES)
).to.be.revertedWith('Vote deadline passed');
});
it('should reject proposal successfully', async function () {
await expect(govern.confirmParamProposal(proposalId))
.to.emit(govern, 'ConfirmParamProposal')
.withArgs(proposalId, false, paramType, paramValue);
const val = await staking.params(consts.ENUM_MAX_VALIDATOR_NUM);
expect(val).to.equal(consts.MAX_VALIDATOR_NUM);
});
});
});
});
});
================================================
FILE: test/GovernedOwner.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, solidityPackedKeccak256, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Bridge, GovernedOwnerProxy, PeggedTokenBridge, SimpleGovernance, TestERC20 } from '../typechain';
import { advanceBlockTime, deployBridgeContracts, deployGovernedOwner, getAccounts } from './lib/common';
import * as consts from './lib/constants';
describe('GovernedOwner Tests', function () {
const initVoterNum = 4;
const InitFastPassThreshold = 40;
const abiCoder = AbiCoder.defaultAbiCoder();
async function fixture() {
const [admin] = await ethers.getSigners();
const { bridge, token, pegBridge } = await deployBridgeContracts(admin);
const { gov, proxy } = await deployGovernedOwner(admin, initVoterNum);
return { admin, bridge, token, pegBridge, gov, proxy };
}
let bridge: Bridge;
let token: TestERC20;
let pegBridge: PeggedTokenBridge;
let gov: SimpleGovernance;
let proxy: GovernedOwnerProxy;
let voters: Wallet[];
beforeEach(async () => {
const res = await loadFixture(fixture);
bridge = res.bridge;
token = res.token;
pegBridge = res.pegBridge;
gov = res.gov;
proxy = res.proxy;
voters = await getAccounts(res.admin, [], 5);
const govAddress = await gov.getAddress();
await bridge.transferOwnership(govAddress);
await pegBridge.transferOwnership(govAddress);
});
it('should pass param change tests', async function () {
expect(await gov.params(consts.GovParamFastPassThreshold)).to.equal(InitFastPassThreshold);
const newThreshold = 10;
await expect(gov.connect(voters[0]).createParamChangeProposal(consts.GovParamFastPassThreshold, newThreshold))
.to.emit(gov, 'ParamChangeProposalCreated')
.withArgs(0, consts.GovParamFastPassThreshold, 10);
const data = abiCoder.encode(['uint8', 'uint256'], [consts.GovParamFastPassThreshold, 10]);
await expect(
gov.connect(voters[2]).executeProposal(0, consts.GovInternalParamChange, ZeroAddress, data)
).to.be.revertedWith('not enough votes');
await gov.connect(voters[1]).voteProposal(0, true);
await expect(gov.connect(voters[2]).executeProposal(0, consts.GovInternalParamChange, ZeroAddress, data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(0);
expect(await gov.params(consts.GovParamFastPassThreshold)).to.equal(newThreshold);
});
it('should pass voter and proxy update tests', async function () {
// voter update tests
expect((await gov.getVoters())[0]).to.deep.equal([
voters[0].address,
voters[1].address,
voters[2].address,
voters[3].address
]);
await expect(gov.connect(voters[0]).createVoterUpdateProposal([voters[1].address, voters[4].address], [0, 100]))
.to.emit(gov, 'VoterUpdateProposalCreated')
.withArgs(0, [voters[1].address, voters[4].address], [0, 100]);
await gov.connect(voters[1]).voteProposal(0, true);
let data = abiCoder.encode(
['address[]', 'uint256[]'],
[
[voters[1].address, voters[4].address],
[0, 100]
]
);
await expect(gov.connect(voters[2]).executeProposal(0, consts.GovInternalVoterUpdate, ZeroAddress, data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(0);
expect((await gov.getVoters())[0]).to.deep.equal([
voters[0].address,
voters[3].address,
voters[2].address,
voters[4].address
]);
expect(await gov.voterPowers(voters[1].address)).to.equal(0);
expect(await gov.voterPowers(voters[4].address)).to.equal(100);
// proxy update tests
expect(await gov.proposerProxies(proxy.getAddress())).to.equal(true);
await expect(
gov.connect(voters[4]).createProxyUpdateProposal([proxy.getAddress(), voters[3].address], [false, true])
)
.to.emit(gov, 'ProxyUpdateProposalCreated')
.withArgs(1, [await proxy.getAddress(), voters[3].address], [false, true]);
await gov.connect(voters[0]).voteProposal(1, true);
data = abiCoder.encode(
['address[]', 'bool[]'],
[
[await proxy.getAddress(), voters[3].address],
[false, true]
]
);
await expect(gov.connect(voters[2]).executeProposal(1, consts.GovInternalProxyUpdate, ZeroAddress, data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(1);
expect(await gov.proposerProxies(proxy.getAddress())).to.equal(false);
expect(await gov.proposerProxies(voters[3].address)).to.equal(true);
});
it('should pass transfer token tests', async function () {
expect(await token.balanceOf(voters[0].address)).to.equal(0);
const amount = parseUnits('1');
await token.transfer(gov.getAddress(), amount);
await expect(gov.connect(voters[0]).createTransferTokenProposal(voters[0].address, token.getAddress(), amount))
.to.emit(gov, 'TransferTokenProposalCreated')
.withArgs(0, voters[0].address, token.getAddress(), amount);
await gov.connect(voters[1]).voteProposal(0, true);
const data = abiCoder.encode(
['address', 'address', 'uint256'],
[voters[0].address, await token.getAddress(), amount]
);
await expect(gov.connect(voters[2]).executeProposal(0, consts.GovInternalTokenTransfer, ZeroAddress, data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(0);
expect(await token.balanceOf(voters[0].address)).to.equal(amount);
});
it('should pass common owner proxy tests', async function () {
// proposal 0: add pauser - success
const pauser = voters[2].address;
await expect(proxy.connect(voters[0]).proposeUpdatePauser(bridge.getAddress(), 1, pauser))
.to.emit(proxy, 'UpdatePauserProposalCreated')
.withArgs(0, bridge.getAddress(), 1, pauser);
let data = (await bridge.addPauser.populateTransaction(pauser)).data || '';
await expect(
gov.connect(voters[3]).executeProposal(0, consts.GovExternalFastPass, bridge.getAddress(), '0x01')
).to.be.revertedWith('data hash not match');
await expect(
gov.connect(voters[3]).executeProposal(0, consts.GovExternalFastPass, voters[0].address, data)
).to.be.revertedWith('data hash not match');
await expect(gov.connect(voters[3]).executeProposal(0, consts.GovExternalFastPass, bridge.getAddress(), data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(0)
.to.emit(bridge, 'PauserAdded')
.withArgs(pauser);
// proposal 1: remove pauser - fail due to timeout
await expect(proxy.connect(voters[0]).proposeUpdatePauser(bridge.getAddress(), 2, pauser))
.to.emit(proxy, 'UpdatePauserProposalCreated')
.withArgs(1, bridge.getAddress(), 2, pauser);
await advanceBlockTime(10000);
data = (await bridge.removePauser.populateTransaction(pauser)).data || '';
await expect(
gov.connect(voters[1]).executeProposal(1, consts.GovExternalFastPass, bridge.getAddress(), data)
).to.be.revertedWith('deadline passed');
await expect(gov.connect(voters[1]).voteProposal(1, true)).to.be.revertedWith('deadline passed');
// proposal 2: transfer ownership
expect(await bridge.owner()).to.equal(await gov.getAddress());
const newOwner = voters[0].address;
await expect(proxy.connect(voters[0]).proposeTransferOwnership(bridge.getAddress(), newOwner))
.to.emit(proxy, 'TransferOwnershipProposalCreated')
.withArgs(2, bridge.getAddress(), newOwner);
data = (await bridge.transferOwnership.populateTransaction(newOwner)).data || '';
await expect(
gov.connect(voters[1]).executeProposal(2, consts.GovExternalDefault, bridge.getAddress(), data)
).to.be.revertedWith('not enough votes');
await gov.connect(voters[1]).voteProposal(2, true);
await expect(gov.voteProposal(0, true)).to.be.revertedWith('invalid voter');
await gov.connect(voters[2]).executeProposal(2, consts.GovExternalDefault, bridge.getAddress(), data);
expect(await bridge.owner()).to.equal(newOwner);
await expect(
gov.connect(voters[2]).executeProposal(2, consts.GovExternalDefault, bridge.getAddress(), data)
).to.be.revertedWith('deadline passed');
});
it('should pass bridge owner proxy tests', async function () {
// reset signers
await expect(proxy.connect(voters[0]).proposeResetSigners(bridge.getAddress(), [voters[0].address], [1000]))
.to.emit(proxy, 'ResetSignersProposalCreated')
.withArgs(0, bridge.getAddress(), [voters[0].address], [1000]);
await gov.connect(voters[1]).voteProposal(0, true);
let data = (await bridge.resetSigners.populateTransaction([voters[0].address], [1000])).data || '';
await expect(gov.connect(voters[2]).executeProposal(0, consts.GovExternalDefault, bridge.getAddress(), data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(0)
.to.emit(bridge, 'SignersUpdated')
.withArgs([voters[0].address], [1000]);
expect(await bridge.ssHash()).to.equal(
solidityPackedKeccak256(['address[]', 'uint256[]'], [[voters[0].address], [1000]])
);
// update governor
await expect(proxy.connect(voters[0]).proposeUpdateGovernor(bridge.getAddress(), 1, voters[0].address))
.to.emit(proxy, 'UpdateGovernorProposalCreated')
.withArgs(1, bridge.getAddress(), 1, voters[0].address);
data = (await bridge.addGovernor.populateTransaction(voters[0].address)).data || '';
await expect(gov.connect(voters[2]).executeProposal(1, consts.GovExternalFastPass, bridge.getAddress(), data))
.to.emit(gov, 'ProposalExecuted')
.withArgs(1)
.to.emit(bridge, 'GovernorAdded')
.withArgs(voters[0].address);
});
});
================================================
FILE: test/Message.spec.ts
================================================
import { expect } from 'chai';
import {
AbiCoder,
AbstractSigner,
getBytes,
parseUnits,
solidityPacked,
solidityPackedKeccak256,
toNumber
} from 'ethers';
import { ethers } from 'hardhat';
import { HardhatEthersSigner, SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Bridge, MessageBus, MsgTest, TestERC20 } from '../typechain';
import { deployMessageContracts } from './lib/common';
import * as consts from './lib/constants';
import { calculateSignatures, getRelayRequest } from './lib/proto';
type RouteInfoStruct = {
sender: string;
receiver: string;
srcChainId: number;
srcTxHash: string;
};
type RouteInfo2Struct = {
sender: Uint8Array;
receiver: string;
srcChainId: number;
srcTxHash: string;
};
const abiCoder = AbiCoder.defaultAbiCoder();
async function computeMessageIdAndSigs(
chainId: number,
msgbus: string,
route: RouteInfoStruct,
message: string,
signers: AbstractSigner[]
) {
const messageId = solidityPackedKeccak256(
['uint8', 'address', 'address', 'uint64', 'bytes32', 'uint64', 'bytes'],
[consts.TYPE_MSG_ONLY, route.sender, route.receiver, route.srcChainId, route.srcTxHash, chainId, message]
);
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, msgbus, 'Message']);
const signedData = solidityPacked(['bytes32', 'bytes32'], [domain, messageId]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { messageId, sigs };
}
async function computeMessage2IdAndSigs(
chainId: number,
msgbus: string,
route: RouteInfo2Struct,
message: string,
signers: AbstractSigner[]
) {
const messageId = solidityPackedKeccak256(
['uint8', 'bytes', 'address', 'uint64', 'bytes32', 'uint64', 'bytes'],
[consts.TYPE_MSG_ONLY, route.sender, route.receiver, route.srcChainId, route.srcTxHash, chainId, message]
);
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, msgbus, 'Message2']);
const signedData = solidityPacked(['bytes32', 'bytes32'], [domain, messageId]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { messageId, sigs };
}
async function getMessageWithTransferRequest(
chainId: number,
msgbus: string,
bridge: string,
sender: string,
receiver: string,
token: string,
amount: bigint,
srcChainId: number,
refId: string,
srcTxHash: string,
message: string,
signer: SignerWithAddress,
power: bigint
) {
const relayReq = await getRelayRequest(
sender,
receiver,
token,
amount,
srcChainId,
chainId,
refId, // fake src transfer Id
[signer],
bridge
);
const bridgeTransferParams = {
request: relayReq.relayBytes,
sigs: relayReq.sigs,
signers: [signer.address],
powers: [power]
};
const xferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'bytes32'],
[sender, receiver, token, amount, srcChainId, chainId, refId]
);
const messageId = solidityPackedKeccak256(['uint8', 'address', 'bytes32'], [consts.TYPE_MSG_XFER, bridge, xferId]);
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, msgbus, 'MessageWithTransfer']);
const signedData = solidityPacked(
['bytes32', 'bytes32', 'bytes', 'bytes32'],
[domain, messageId, message, srcTxHash]
);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures([signer], getBytes(signedDataHash));
const transferInfo = {
t: consts.XFER_TYPE_LQ_RELAY,
sender: sender,
receiver: receiver,
token: token,
amount: amount,
wdseq: 0,
srcChainId: srcChainId,
refId: refId,
srcTxHash: srcTxHash
};
const executionParams = {
message: message,
transfer: transferInfo,
sigs: sigs,
signers: [signer.address],
powers: [power]
};
return { bridgeTransferParams, executionParams, messageId };
}
describe('Message Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { bridge, msgBus, msgTest, token } = await deployMessageContracts(admin);
await bridge.resetSigners([admin.address], [parseUnits('1')]);
return { admin, bridge, msgBus, msgTest, token };
}
let admin: HardhatEthersSigner;
let bridge: Bridge;
let msgBus: MessageBus;
let msgTest: MsgTest;
let token: TestERC20;
let chainId: number;
beforeEach(async () => {
const res = await loadFixture(fixture);
admin = res.admin;
bridge = res.bridge;
token = res.token;
msgBus = res.msgBus;
msgTest = res.msgTest;
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
});
it('should execute msg correctly', async function () {
const hash = solidityPackedKeccak256(['string'], ['hello']);
const srcChainId = 5;
const routeInfo = {
sender: admin.address,
receiver: await msgTest.getAddress(),
srcChainId: srcChainId,
srcTxHash: hash // fake tx hash
};
let nonce = 1;
let message = abiCoder.encode(['uint64', 'bytes'], [nonce, hash]);
let res = await computeMessageIdAndSigs(chainId, await msgBus.getAddress(), routeInfo, message, [admin]);
await expect(
msgBus.getFunction('executeMessage(bytes,(address,address,uint64,bytes32),bytes[],address[],uint256[])')(
message,
routeInfo,
res.sigs,
[admin.address],
[parseUnits('1')]
)
)
.to.emit(msgTest, 'MessageReceived')
.withArgs(admin.address, srcChainId, nonce, hash)
.to.emit(msgBus, 'Executed')
.withArgs(
consts.TYPE_MSG_ONLY,
res.messageId,
consts.MSG_TX_SUCCESS,
await msgTest.getAddress(),
srcChainId,
hash
);
const routeInfo2 = {
sender: getBytes(admin.address),
receiver: await msgTest.getAddress(),
srcChainId: srcChainId,
srcTxHash: hash // fake tx hash
};
nonce = 2;
message = abiCoder.encode(['uint64', 'bytes'], [nonce, hash]);
res = await computeMessage2IdAndSigs(chainId, await msgBus.getAddress(), routeInfo2, message, [admin]);
await expect(
msgBus.getFunction('executeMessage(bytes,(bytes,address,uint64,bytes32),bytes[],address[],uint256[])')(
message,
routeInfo2,
res.sigs,
[admin.address],
[parseUnits('1')]
)
)
.to.emit(msgTest, 'Message2Received')
.withArgs(solidityPacked(['address'], [admin.address]), srcChainId, nonce, hash)
.to.emit(msgBus, 'Executed')
.withArgs(
consts.TYPE_MSG_ONLY,
res.messageId,
consts.MSG_TX_SUCCESS,
await msgTest.getAddress(),
srcChainId,
hash
);
nonce = 100000000000001;
message = abiCoder.encode(['uint64', 'bytes'], [nonce, hash]);
res = await computeMessageIdAndSigs(chainId, await msgBus.getAddress(), routeInfo, message, [admin]);
await expect(
msgBus.getFunction('executeMessage(bytes,(address,address,uint64,bytes32),bytes[],address[],uint256[])')(
message,
routeInfo,
res.sigs,
[admin.address],
[parseUnits('1')]
)
)
.to.emit(msgBus, 'CallReverted')
.withArgs('invalid nonce')
.to.emit(msgBus, 'Executed')
.withArgs(consts.TYPE_MSG_ONLY, res.messageId, consts.MSG_TX_FAIL, await msgTest.getAddress(), srcChainId, hash);
nonce = 100000000000002;
message = abiCoder.encode(['uint64', 'bytes'], [nonce, hash]);
res = await computeMessageIdAndSigs(chainId, await msgBus.getAddress(), routeInfo, message, [admin]);
await expect(
msgBus.getFunction('executeMessage(bytes,(address,address,uint64,bytes32),bytes[],address[],uint256[])')(
message,
routeInfo,
res.sigs,
[admin.address],
[parseUnits('1')]
)
)
.to.emit(msgBus, 'CallReverted')
.withArgs('Transaction reverted silently')
.to.emit(msgBus, 'Executed')
.withArgs(consts.TYPE_MSG_ONLY, res.messageId, consts.MSG_TX_FAIL, await msgTest.getAddress(), srcChainId, hash);
nonce = 100000000000004;
message = abiCoder.encode(['uint64', 'bytes'], [nonce, hash]);
res = await computeMessageIdAndSigs(chainId, await msgBus.getAddress(), routeInfo, message, [admin]);
await expect(
msgBus.getFunction('executeMessage(bytes,(address,address,uint64,bytes32),bytes[],address[],uint256[])')(
message,
routeInfo,
res.sigs,
[admin.address],
[parseUnits('1')]
)
).to.be.revertedWith('MSG::ABORT:invalid nonce');
});
it('should execute msg with transfer correctly', async function () {
await token.approve(bridge.getAddress(), parseUnits('100'));
await bridge.addLiquidity(token.getAddress(), parseUnits('50'));
const hash = solidityPackedKeccak256(['string'], ['hello']);
const amount = parseUnits('1');
const srcChainId = 5;
const message = abiCoder.encode(['address', 'bytes'], [admin.address, hash]);
const res = await getMessageWithTransferRequest(
chainId,
await msgBus.getAddress(),
await bridge.getAddress(),
admin.address,
await msgTest.getAddress(),
await token.getAddress(),
amount,
srcChainId,
hash, // fake src transfer Id
hash, // fake src srcTxHash
message,
admin,
parseUnits('1')
);
await expect(msgBus.transferAndExecuteMsg(res.bridgeTransferParams, res.executionParams))
.to.emit(msgBus, 'Executed')
.withArgs(
consts.TYPE_MSG_XFER,
res.messageId,
consts.MSG_TX_SUCCESS,
await msgTest.getAddress(),
srcChainId,
hash
)
.to.emit(msgTest, 'MessageReceivedWithTransfer')
.withArgs(await token.getAddress(), amount, admin.address, srcChainId, admin.address, hash);
await expect(
msgBus.executeMessageWithTransfer(
message,
res.executionParams.transfer,
res.executionParams.sigs,
res.executionParams.signers,
res.executionParams.powers
)
).to.be.revertedWith('transfer already executed');
});
});
================================================
FILE: test/MultiValidator.spec.ts
================================================
import { expect } from 'chai';
import { parseUnits, Wallet } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Staking, TestERC20 } from '../typechain';
import { advanceBlockNumber, deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
describe('Multiple validators Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, celr } = await deployContracts(admin);
return { admin, staking, celr };
}
let staking: Staking;
let celr: TestERC20;
let validators: Wallet[];
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
celr = res.celr;
validators = await getAccounts(res.admin, [celr], 8);
await celr.approve(staking.getAddress(), parseUnits('100'));
const stakes = [
parseUnits('6'),
parseUnits('2'),
parseUnits('10'),
parseUnits('5'),
parseUnits('7'),
parseUnits('3'),
parseUnits('9')
];
for (let i = 0; i < 8; i++) {
await celr.connect(validators[i]).approve(staking.getAddress(), parseUnits('100'));
await staking
.connect(validators[i])
.initializeValidator(validators[i].address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
if (i < 7) {
await staking.connect(validators[i]).delegate(validators[i].address, consts.VALIDATOR_STAKE);
await staking.delegate(validators[i].address, stakes[i]);
await staking.connect(validators[i]).bondValidator();
}
}
});
it('should getQuorumTokens successfully', async function () {
const quorum = await staking.getQuorumTokens();
expect(quorum).to.equal(parseUnits('42') + 1n);
});
it('should fail to bondValidator before delegating enough stake', async function () {
await staking.connect(validators[7]).delegate(validators[7].address, parseUnits('2'));
await expect(staking.connect(validators[7]).bondValidator()).to.be.revertedWith('Insufficient tokens');
});
it('should replace a current validator by calling bondValidator with enough stake', async function () {
await staking.connect(validators[7]).delegate(validators[7].address, parseUnits('8'));
await expect(staking.connect(validators[7]).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[1].address, consts.STATUS_UNBONDING)
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[7].address, consts.STATUS_BONDED);
const quorum = await staking.getQuorumTokens();
expect(quorum).to.equal((parseUnits('68') * 2n) / 3n + 1n);
});
it('should remove validator due to undelegate and add new validator successfully', async function () {
await expect(staking.connect(validators[1]).undelegateShares(validators[1].address, parseUnits('2')))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[1].address, consts.STATUS_UNBONDING);
let quorum = await staking.getQuorumTokens();
expect(quorum).to.equal((parseUnits('58') * 2n) / 3n + 1n);
await staking.connect(validators[7]).delegate(validators[7].address, parseUnits('8'));
await expect(staking.connect(validators[7]).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[7].address, consts.STATUS_BONDED);
quorum = await staking.getQuorumTokens();
expect(quorum).to.equal((parseUnits('68') * 2n) / 3n + 1n);
});
describe('after one validator is replaced', async () => {
beforeEach(async () => {
await staking.connect(validators[7]).delegate(validators[7].address, parseUnits('8'));
await staking.connect(validators[7]).bondValidator();
});
it('should confirmUnbondedValidator only after unbondBlock', async function () {
const res = await staking.validators(validators[1].address);
expect(res.status).to.equal(consts.STATUS_UNBONDING);
await expect(staking.confirmUnbondedValidator(validators[1].address)).to.be.revertedWith(
'Unbond block not reached'
);
await advanceBlockNumber(consts.UNBONDING_PERIOD);
await expect(staking.confirmUnbondedValidator(validators[1].address))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[1].address, consts.STATUS_UNBONDED);
});
it('should replace current min stake validator with the unbonding validator', async function () {
await staking.connect(validators[1]).delegate(validators[1].address, parseUnits('5'));
await expect(staking.connect(validators[1]).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[5].address, consts.STATUS_UNBONDING)
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[1].address, consts.STATUS_BONDED);
await staking.delegate(validators[1].address, parseUnits('5'));
await staking.delegate(validators[5].address, parseUnits('3'));
const quorum = await staking.getQuorumTokens();
expect(quorum).to.equal((parseUnits('77') * 2n) / 3n + 1n);
});
});
});
================================================
FILE: test/Sentinel.spec.ts
================================================
import { expect } from 'chai';
import { Wallet } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Bridge, PeggedTokenBridge, Sentinel } from '../typechain';
import { deployBridgeContracts, deploySentinel, getAccounts } from './lib/common';
describe('Sentinel Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { bridge, pegBridge } = await deployBridgeContracts(admin);
const sentinel = await deploySentinel(admin);
return { admin, bridge, pegBridge, sentinel };
}
let bridge: Bridge;
let pegBridge: PeggedTokenBridge;
let sentinel: Sentinel;
let guards: Wallet[];
let pausers: Wallet[];
let governor: Wallet;
beforeEach(async () => {
const res = await loadFixture(fixture);
const accounts = await getAccounts(res.admin, [], 5);
bridge = res.bridge;
pegBridge = res.pegBridge;
sentinel = res.sentinel;
guards = [accounts[0], accounts[1]];
pausers = [accounts[2], accounts[3]];
governor = accounts[4];
const sentinelAddress = await sentinel.getAddress();
await sentinel.updateGuards([guards[0].address, guards[1].address], [], 2);
await sentinel.addPausers([pausers[0].address, pausers[1].address], [1, 2]);
await sentinel.addGovernors([governor.address]);
await bridge.addPauser(sentinelAddress);
await pegBridge.addPauser(sentinelAddress);
await bridge.addGovernor(sentinelAddress);
});
it('should pass guard tests', async function () {
await expect(sentinel.updateGuards([pausers[0].address], [], 2))
.to.emit(sentinel, 'GuardUpdated')
.withArgs(pausers[0].address, 1);
expect(await sentinel.numGuards()).to.equal(3);
expect(await sentinel.relaxThreshold()).to.equal(2);
expect(await sentinel.numRelaxedGuards()).to.equal(0);
await sentinel.connect(guards[0]).relax();
await expect(sentinel.connect(guards[1]).relax()).to.emit(sentinel, 'RelaxStatusUpdated').withArgs(true);
await expect(sentinel.updateGuards([pausers[1].address], [], 3))
.to.emit(sentinel, 'RelaxStatusUpdated')
.withArgs(false);
expect(await sentinel.numGuards()).to.equal(4);
expect(await sentinel.relaxThreshold()).to.equal(3);
expect(await sentinel.numRelaxedGuards()).to.equal(2);
await expect(sentinel.updateGuards([], [guards[0].address, pausers[0].address], 2))
.to.emit(sentinel, 'GuardUpdated')
.withArgs(guards[0].address, 0)
.to.emit(sentinel, 'GuardUpdated')
.withArgs(pausers[0].address, 0);
expect(await sentinel.numGuards()).to.equal(2);
expect(await sentinel.relaxThreshold()).to.equal(2);
expect(await sentinel.numRelaxedGuards()).to.equal(1);
await expect(sentinel.updateGuards([], [pausers[1].address], 1))
.to.emit(sentinel, 'RelaxStatusUpdated')
.withArgs(true);
expect(await sentinel.numGuards()).to.equal(1);
expect(await sentinel.relaxThreshold()).to.equal(1);
expect(await sentinel.numRelaxedGuards()).to.equal(1);
});
it('should pass pauser tests', async function () {
await expect(sentinel.connect(governor).getFunction('pause(address)')(bridge.getAddress())).to.be.revertedWith(
'invalid caller'
);
await expect(
sentinel.connect(pausers[1]).getFunction('pause(address[])')([bridge.getAddress(), pegBridge.getAddress()])
)
.to.emit(bridge, 'Paused')
.to.emit(pegBridge, 'Paused');
await expect(
sentinel.connect(pausers[0]).getFunction('pause(address[])')([bridge.getAddress(), pegBridge.getAddress()])
).to.be.revertedWith('pause failed for all targets');
await expect(
sentinel.connect(pausers[0]).getFunction('unpause(address[])')([bridge.getAddress(), pegBridge.getAddress()])
).to.be.revertedWith('not in relaxed mode');
await sentinel.connect(guards[0]).relax();
await expect(sentinel.connect(pausers[0]).getFunction('unpause(address)')(bridge.getAddress())).to.be.revertedWith(
'not in relaxed mode'
);
await sentinel.connect(guards[1]).relax();
await expect(sentinel.connect(pausers[1]).getFunction('unpause(address)')(bridge.getAddress())).to.be.revertedWith(
'invalid caller'
);
await expect(sentinel.connect(pausers[0]).getFunction('unpause(address)')(bridge.getAddress())).to.emit(
bridge,
'Unpaused'
);
await expect(
sentinel.connect(pausers[0]).getFunction('unpause(address[])')([bridge.getAddress(), pegBridge.getAddress()])
)
.to.emit(pegBridge, 'Unpaused')
.to.emit(sentinel, 'Failed')
.withArgs(await bridge.getAddress(), 'Pausable: not paused');
});
it('should pass governor tests', async function () {
await expect(sentinel.connect(governor).setDelayPeriod(bridge.getAddress(), 10))
.to.emit(bridge, 'DelayPeriodUpdated')
.withArgs(10);
await expect(sentinel.connect(governor).setDelayPeriod(bridge.getAddress(), 5)).to.be.revertedWith(
'not in relax mode, can only increase period'
);
await sentinel.connect(guards[0]).relax();
await sentinel.connect(guards[1]).relax();
await expect(sentinel.connect(governor).setDelayPeriod(bridge.getAddress(), 5))
.to.emit(bridge, 'DelayPeriodUpdated')
.withArgs(5);
});
});
================================================
FILE: test/Slash.spec.ts
================================================
import { expect } from 'chai';
import { parseUnits, toNumber, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Staking, StakingReward, TestERC20 } from '../typechain';
import { advanceBlockNumber, deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
import { getSlashRequest } from './lib/proto';
describe('Slash Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, stakingReward, celr } = await deployContracts(admin);
return { admin, staking, stakingReward, celr };
}
let staking: Staking;
let reward: StakingReward;
let celr: TestERC20;
let admin: HardhatEthersSigner;
let validators: Wallet[];
let signers: Wallet[];
let expireTime: number;
let chainId: number;
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
reward = res.stakingReward;
celr = res.celr;
admin = res.admin;
const accounts = await getAccounts(res.admin, [celr], 7);
validators = [accounts[0], accounts[1], accounts[2], accounts[3]];
signers = [accounts[0], accounts[4], accounts[5], accounts[6]];
await staking.setRewardContract(reward.getAddress());
await celr.approve(staking.getAddress(), parseUnits('100'));
for (let i = 0; i < 4; i++) {
await celr.connect(validators[i]).approve(staking.getAddress(), parseUnits('100'));
await staking
.connect(validators[i])
.initializeValidator(signers[i].address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
await staking.delegate(validators[i].address, consts.DELEGATOR_STAKE);
await staking.connect(validators[i]).bondValidator();
const blockNumber = await ethers.provider.getBlockNumber();
const blockTime = (await ethers.provider.getBlock(blockNumber))!.timestamp;
expireTime = blockTime + 100;
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
}
});
it('should slash successfully (only once using the same nonce)', async function () {
const adminBalanceBefore = await celr.balanceOf(admin.address);
const val1BalanceBefore = await celr.balanceOf(validators[1].address);
const rewardPoolBefore = await celr.balanceOf(reward.getAddress());
const request = await getSlashRequest(
validators[0].address,
1,
consts.SLASH_FACTOR,
expireTime,
0,
[validators[1].address, ZeroAddress],
[parseUnits('0.1'), parseUnits('0.01')],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs))
.to.emit(staking, 'DelegationUpdate')
.withArgs(validators[0].address, ZeroAddress, parseUnits('7.6'), 0, parseUnits('-0.4'))
.to.emit(staking, 'Slash')
.withArgs(validators[0].address, 1, parseUnits('0.4'))
.to.emit(staking, 'SlashAmtCollected')
.withArgs(admin.address, parseUnits('0.01'))
.to.emit(staking, 'SlashAmtCollected')
.withArgs(validators[1].address, parseUnits('0.1'));
await staking.collectForfeiture();
const adminBalanceAfter = await celr.balanceOf(admin.address);
const val1BalanceAfter = await celr.balanceOf(validators[1].address);
const rewardPoolAfter = await celr.balanceOf(reward.getAddress());
expect(adminBalanceAfter - parseUnits('0.01')).to.equal(adminBalanceBefore);
expect(val1BalanceAfter - parseUnits('0.1')).to.equal(val1BalanceBefore);
expect(rewardPoolAfter - parseUnits('0.29')).to.equal(rewardPoolBefore);
await expect(staking.slash(request.slashBytes, request.sigs)).to.be.revertedWith('Used slash nonce');
});
it('should slash successfully with undelegations and redelegations', async function () {
await staking.undelegateShares(validators[0].address, parseUnits('1'));
await staking.undelegateTokens(validators[0].address, parseUnits('2'));
await staking.connect(validators[0]).undelegateShares(validators[0].address, parseUnits('1'));
const request = await getSlashRequest(
validators[0].address,
1,
consts.SLASH_FACTOR,
expireTime,
0,
[],
[],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs))
.to.emit(staking, 'DelegationUpdate')
.withArgs(validators[0].address, ZeroAddress, parseUnits('3.8'), 0, parseUnits('-0.2'))
.to.emit(staking, 'Slash')
.withArgs(validators[0].address, 1, parseUnits('0.4'));
// check and complete pending undelegations
let dinfo = await staking.getDelegatorInfo(validators[0].address, admin.address);
expect(dinfo.tokens).to.equal(parseUnits('2.85'));
expect(dinfo.shares).to.equal(parseUnits('3'));
expect(dinfo.undelegations[0].shares).to.equal(parseUnits('1'));
expect(dinfo.undelegations[1].shares).to.equal(parseUnits('2'));
await advanceBlockNumber(consts.UNBONDING_PERIOD);
await expect(staking.completeUndelegate(validators[0].address))
.to.emit(staking, 'Undelegated')
.withArgs(validators[0].address, admin.address, parseUnits('2.85'));
// do additional undelegation
await expect(staking.undelegateShares(validators[0].address, parseUnits('1')))
.to.emit(staking, 'DelegationUpdate')
.withArgs(validators[0].address, admin.address, parseUnits('2.85'), parseUnits('2'), parseUnits('-0.95'));
await advanceBlockNumber(consts.UNBONDING_PERIOD);
await expect(staking.completeUndelegate(validators[0].address))
.to.emit(staking, 'Undelegated')
.withArgs(validators[0].address, admin.address, parseUnits('0.95'));
// redelegate
await expect(staking.delegate(validators[0].address, parseUnits('1')))
.to.emit(staking, 'DelegationUpdate')
.withArgs(
validators[0].address,
admin.address,
parseUnits('3.85'),
parseUnits('3052631578947368421', 'wei'),
parseUnits('1')
);
await expect(staking.undelegateShares(validators[0].address, parseUnits('1052631578947368419', 'wei')))
.to.emit(staking, 'DelegationUpdate')
.withArgs(
validators[0].address,
admin.address,
parseUnits('2850000000000000002', 'wei'),
parseUnits('2000000000000000002', 'wei'),
parseUnits('-999999999999999998', 'wei')
);
await expect(staking.undelegateTokens(validators[0].address, parseUnits('1'))).to.be.revertedWith(
'not enough remaining shares'
);
await expect(staking.undelegateTokens(validators[0].address, parseUnits('1900000000000000001', 'wei')))
.to.emit(staking, 'DelegationUpdate')
.withArgs(
validators[0].address,
admin.address,
parseUnits('950000000000000001', 'wei'),
0,
parseUnits('-1900000000000000001', 'wei')
);
dinfo = await staking.getDelegatorInfo(validators[0].address, admin.address);
expect(dinfo.tokens).to.equal(0);
expect(dinfo.shares).to.equal(0);
});
it('should unbond validator due to slash', async function () {
const request = await getSlashRequest(
validators[0].address,
1,
1e5,
expireTime,
0,
[],
[],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs))
.to.emit(staking, 'DelegationUpdate')
.withArgs(validators[0].address, ZeroAddress, parseUnits('7.2'), 0, parseUnits('-0.8'))
.to.emit(staking, 'Slash')
.withArgs(validators[0].address, 1, parseUnits('0.8'))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[0].address, consts.STATUS_UNBONDING);
});
it('should unbond validator due to slash with jail period', async function () {
const request = await getSlashRequest(
validators[0].address,
1,
0,
expireTime,
10,
[],
[],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs))
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[0].address, consts.STATUS_UNBONDING)
.to.emit(staking, 'DelegationUpdate')
.withArgs(validators[0].address, ZeroAddress, parseUnits('8'), 0, 0)
.to.emit(staking, 'Slash')
.withArgs(validators[0].address, 1, 0);
await expect(staking.connect(validators[0]).bondValidator()).to.be.revertedWith('Bond block not reached');
await advanceBlockNumber(10);
await expect(staking.connect(validators[0]).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[0].address, consts.STATUS_BONDED);
});
it('should fail to slash with invalid requests', async function () {
let request = await getSlashRequest(
validators[0].address,
1,
consts.SLASH_FACTOR,
expireTime,
0,
[],
[],
[signers[1], signers[2]],
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs)).to.be.revertedWith('Quorum not reached');
request = await getSlashRequest(
validators[0].address,
1,
consts.SLASH_FACTOR,
0,
0,
[],
[],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs)).to.be.revertedWith('Slash expired');
});
it('should fail to slash when paused or slash disabled', async function () {
await staking.pause();
const request = await getSlashRequest(
validators[0].address,
1,
consts.SLASH_FACTOR,
expireTime,
0,
[],
[],
signers,
chainId,
await staking.getAddress()
);
await expect(staking.slash(request.slashBytes, request.sigs)).to.be.revertedWith('Pausable: paused');
await staking.unpause();
await staking.setMaxSlashFactor(0);
await expect(staking.slash(request.slashBytes, request.sigs)).to.be.revertedWith('Exceed max slash factor');
});
});
================================================
FILE: test/StakingReward.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, toNumber, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Staking, StakingReward, TestERC20 } from '../typechain';
import { deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
import { getStakingRewardRequest } from './lib/proto';
describe('StakingReward Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, stakingReward, celr } = await deployContracts(admin);
return { admin, staking, stakingReward, celr };
}
const abiCoder = AbiCoder.defaultAbiCoder();
let staking: Staking;
let reward: StakingReward;
let celr: TestERC20;
let validators: Wallet[];
let signers: Wallet[];
let chainId: number;
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
reward = res.stakingReward;
celr = res.celr;
const accounts = await getAccounts(res.admin, [celr], 6);
validators = [accounts[0], accounts[1], accounts[2], accounts[3]];
signers = [accounts[0], accounts[1], accounts[4], accounts[5]];
for (let i = 0; i < 4; i++) {
await celr.connect(validators[i]).approve(staking.getAddress(), parseUnits('100'));
await celr.connect(validators[i]).approve(reward.getAddress(), parseUnits('100'));
await staking
.connect(validators[i])
.initializeValidator(signers[i].address, consts.MIN_SELF_DELEGATION, consts.COMMISSION_RATE);
await staking.connect(validators[i]).delegate(validators[i].address, consts.MIN_VALIDATOR_TOKENS);
await staking.connect(validators[i]).bondValidator();
}
await reward.connect(validators[0]).contributeToRewardPool(100);
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
});
it('should fail to contribute to reward pool when paused', async function () {
await reward.pause();
await expect(reward.contributeToRewardPool(100)).to.be.revertedWith('Pausable: paused');
});
it('should contribute to reward pool successfully', async function () {
await expect(reward.connect(validators[0]).contributeToRewardPool(100))
.to.emit(reward, 'StakingRewardContributed')
.withArgs(validators[0].address, 100);
});
it('should update the commission rate lock successfully', async function () {
const newRate = consts.COMMISSION_RATE + 10;
const data = abiCoder.encode(['uint256'], [newRate]);
await expect(staking.connect(validators[0]).updateCommissionRate(newRate))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validators[0].address, 'commission', data, ZeroAddress);
});
it('should fail to claim reward when paused', async function () {
await reward.pause();
const r = await getStakingRewardRequest(
validators[0].address,
parseUnits('100', 'wei'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs)).to.be.revertedWith('Pausable: paused');
});
it('should claim reward successfully', async function () {
let r = await getStakingRewardRequest(
validators[0].address,
parseUnits('40', 'wei'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs))
.to.emit(reward, 'StakingRewardClaimed')
.withArgs(validators[0].address, 40);
r = await getStakingRewardRequest(
validators[0].address,
parseUnits('90', 'wei'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs))
.to.emit(reward, 'StakingRewardClaimed')
.withArgs(validators[0].address, 50);
});
it('should fail to claim reward more than amount in reward pool', async function () {
const r = await getStakingRewardRequest(
validators[0].address,
parseUnits('101', 'wei'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs)).to.be.revertedWith(
'ERC20: transfer amount exceeds balance'
);
});
it('should fail to claim reward if there is no new reward', async function () {
const r = await getStakingRewardRequest(
validators[0].address,
parseUnits('0'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs)).to.be.revertedWith('No new reward');
});
it('should fail to claim reward with insufficient signatures', async function () {
const r = await getStakingRewardRequest(
validators[0].address,
parseUnits('10', 'wei'),
[signers[0], signers[1]],
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, r.sigs)).to.be.revertedWith('Quorum not reached');
});
it('should fail to claim reward with disordered signatures', async function () {
const r = await getStakingRewardRequest(
validators[0].address,
parseUnits('10', 'wei'),
signers,
chainId,
await reward.getAddress()
);
await expect(reward.claimReward(r.rewardBytes, [r.sigs[0], r.sigs[2], r.sigs[1], r.sigs[3]])).to.be.revertedWith(
'Signers not in ascending order'
);
await expect(reward.claimReward(r.rewardBytes, [r.sigs[0], r.sigs[0], r.sigs[1], r.sigs[2]])).to.be.revertedWith(
'Signers not in ascending order'
);
});
});
================================================
FILE: test/TransferSwap.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, solidityPackedKeccak256, toNumber, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { Address } from 'hardhat-deploy/types';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { Bridge, DummySwap, TestERC20, TransferSwap, WETH } from '../typechain';
import { deploySwapContracts, getAccounts } from './lib/common';
const UINT64_MAX = '9223372036854775807';
const abiCoder = AbiCoder.defaultAbiCoder();
async function swapFixture() {
const [admin] = await ethers.getSigners();
const res = await deploySwapContracts(admin);
return { admin, ...res };
}
function computeId(sender: string, srcChainId: number, dstChainId: number, message: string) {
return solidityPackedKeccak256(['address', 'uint64', 'uint64', 'bytes'], [sender, srcChainId, dstChainId, message]);
}
function computeDirectSwapId(sender: string, srcChainId: number, receiver: Address, nonce: number, swap: Swap) {
const swapStruct = [swap.path, swap.dex, swap.deadline, swap.minRecvAmt];
const encoded = abiCoder.encode(
['address', 'uint64', 'address', 'uint64', '(address[], address, uint256, uint256)'],
[sender, srcChainId, receiver, nonce, swapStruct]
);
return solidityPackedKeccak256(['bytes'], [encoded]);
}
function encodeMessage(
dstSwap: { dex: string; path: string[]; deadline: bigint; minRecvAmt: bigint },
receiver: string,
nonce: number,
nativeOut: boolean
) {
const encoded = abiCoder.encode(
['((address[], address , uint256, uint256), address, uint64, bool)'],
[[[dstSwap.path, dstSwap.dex, dstSwap.deadline, dstSwap.minRecvAmt], receiver, nonce, nativeOut]]
);
return encoded;
}
function slip(amount: bigint, perc: number) {
const percent = 100 - perc;
return (amount * parseUnits(percent.toString(), 4)) / parseUnits('100', 4);
}
let tokenA: TestERC20;
let tokenB: TestERC20;
let xswap: TransferSwap;
let dex: DummySwap;
let bridge: Bridge;
let accounts: Wallet[];
let admin: HardhatEthersSigner;
let chainId: number;
let sender: Wallet;
let receiver: Wallet;
let weth: WETH;
let amountIn: bigint;
const maxBridgeSlippage = parseUnits('100', 4); // 100%
const expectNonce = 1;
interface Swap {
dex: string;
path: string[];
deadline: bigint;
minRecvAmt: bigint;
}
let srcSwap: Swap;
let dstSwap: Swap;
async function prepare() {
const res = await loadFixture(swapFixture);
admin = res.admin;
tokenA = res.tokenA;
tokenB = res.tokenB;
weth = res.weth;
xswap = res.transferSwap;
dex = res.swap;
bridge = res.bridge;
accounts = await getAccounts(res.admin, [tokenA, tokenB], 4);
chainId = toNumber((await ethers.provider.getNetwork()).chainId);
sender = accounts[0];
receiver = accounts[1];
amountIn = parseUnits('100');
srcSwap = {
dex: await dex.getAddress(),
path: [] as string[],
deadline: BigInt(UINT64_MAX),
minRecvAmt: slip(amountIn, 10)
};
dstSwap = {
dex: await dex.getAddress(),
path: [await tokenA.getAddress(), await tokenB.getAddress()],
deadline: BigInt(UINT64_MAX),
minRecvAmt: slip(amountIn, 10)
};
await xswap.setMinSwapAmount(tokenA.getAddress(), parseUnits('10'));
await xswap.setSupportedDex(dex.getAddress(), true);
await dex.setFakeSlippage(parseUnits('5', 4));
await tokenA.connect(res.admin).transfer(dex.getAddress(), parseUnits('1000'));
await tokenB.connect(res.admin).transfer(dex.getAddress(), parseUnits('1000'));
await weth.connect(res.admin).deposit({ value: parseUnits('100') });
await weth.connect(res.admin).transfer(dex.getAddress(), parseUnits('100'));
return { admin, tokenA, tokenB, xswap, dex, bridge, accounts, chainId };
}
describe('Test transferWithSwap', function () {
let srcChainId: number;
const dstChainId = 2; // doesn't matter
beforeEach(async () => {
await prepare();
srcChainId = chainId;
});
it('should revert if paths are empty', async function () {
srcSwap.path = [];
dstSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
await tokenA.connect(sender).approve(xswap.getAddress(), amountIn);
await expect(
xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1)
).to.be.reverted;
});
it('should revert if min swap amount is not satisfied', async function () {
amountIn = parseUnits('5');
srcSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
dstSwap.path = [await tokenB.getAddress(), await tokenA.getAddress()];
await tokenA.connect(sender).approve(xswap.getAddress(), amountIn);
await expect(
xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1)
).to.be.revertedWith('amount must be greater than min swap amount');
});
it('should swap and send', async function () {
srcSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
dstSwap.path = [await tokenB.getAddress(), await tokenA.getAddress()];
await tokenA.connect(sender).approve(xswap.getAddress(), amountIn);
const tx = await xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1);
const message = encodeMessage(dstSwap, sender.address, expectNonce, false);
const expectId = computeId(sender.address, srcChainId, dstChainId, message);
await expect(tx)
.to.emit(xswap, 'SwapRequestSent')
.withArgs(expectId, dstChainId, amountIn, srcSwap.path[0], dstSwap.path[1]);
const expectedSendAmt = slip(amountIn, 5);
const srcXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'uint64'],
[
await xswap.getAddress(),
receiver.address,
await tokenB.getAddress(),
expectedSendAmt,
dstChainId,
expectNonce,
srcChainId
]
);
await expect(tx)
.to.emit(bridge, 'Send')
.withArgs(
srcXferId,
xswap.getAddress(),
receiver.address,
tokenB.getAddress(),
expectedSendAmt,
dstChainId,
expectNonce,
maxBridgeSlippage
);
});
it('should directly send', async function () {
srcSwap.path = [await tokenB.getAddress()];
dstSwap.path = [await tokenB.getAddress(), await tokenA.getAddress()];
await tokenB.connect(sender).approve(xswap.getAddress(), amountIn);
const tx = await xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1);
const message = encodeMessage(dstSwap, sender.address, expectNonce, false);
const id = computeId(sender.address, srcChainId, dstChainId, message);
await expect(tx)
.to.emit(xswap, 'SwapRequestSent')
.withArgs(id, dstChainId, amountIn, srcSwap.path[0], dstSwap.path[1]);
const srcXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'uint64'],
[
await xswap.getAddress(),
receiver.address,
await tokenB.getAddress(),
amountIn,
dstChainId,
expectNonce,
srcChainId
]
);
await expect(tx)
.to.emit(bridge, 'Send')
.withArgs(
srcXferId,
xswap.getAddress(),
receiver.address,
tokenB.getAddress(),
amountIn,
dstChainId,
expectNonce,
maxBridgeSlippage
);
});
it('should directly swap', async function () {
srcSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
dstSwap.path = [];
await tokenA.connect(sender).approve(xswap.getAddress(), amountIn);
const recvBalBefore = await tokenB.connect(receiver).balanceOf(receiver.address);
const tx = await xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, chainId, srcSwap, dstSwap, maxBridgeSlippage, 1);
const recvBalAfter = await tokenB.connect(receiver).balanceOf(receiver.address);
const expectId = computeDirectSwapId(sender.address, srcChainId, receiver.address, expectNonce, srcSwap);
await expect(tx).to.not.emit(xswap, 'SwapRequestSent');
await expect(tx).to.not.emit(bridge, 'Send');
await expect(tx)
.to.emit(xswap, 'DirectSwap')
.withArgs(expectId, chainId, amountIn, tokenA.getAddress(), slip(amountIn, 5), tokenB.getAddress());
expect(recvBalAfter).equal(recvBalBefore + slip(amountIn, 5));
});
it('should revert if the tx results in a noop', async function () {
srcSwap.path = [await tokenA.getAddress()];
dstSwap.path = [];
await tokenA.connect(sender).approve(xswap.getAddress(), amountIn);
await expect(
xswap
.connect(sender)
.transferWithSwap(receiver.address, amountIn, chainId, srcSwap, dstSwap, maxBridgeSlippage, 1)
).to.be.revertedWith('noop is not allowed');
});
});
describe('Test transferWithSwapNative', function () {
let srcChainId: number;
const dstChainId = 2; // doesn't matter
const amountIn2 = parseUnits('10');
beforeEach(async () => {
await prepare();
srcChainId = chainId;
});
it('should revert if native in does not match amountIn (native in)', async function () {
srcSwap.path = [await weth.getAddress(), await tokenB.getAddress()];
dstSwap.path = [await tokenB.getAddress(), await weth.getAddress()];
await expect(
xswap
.connect(sender)
.transferWithSwapNative(receiver.address, amountIn2, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1, true, {
value: amountIn2 / 2n
})
).to.be.revertedWith('Amount insufficient');
});
it('should swap and send (native in)', async function () {
srcSwap.path = [await weth.getAddress(), await tokenB.getAddress()];
srcSwap.minRecvAmt = slip(amountIn2, 10);
dstSwap.path = [await tokenB.getAddress(), await weth.getAddress()];
dstSwap.minRecvAmt = slip(amountIn2, 10);
const balBefore = await ethers.provider.getBalance(sender);
const tx = await xswap
.connect(sender)
.transferWithSwapNative(receiver.address, amountIn2, dstChainId, srcSwap, dstSwap, maxBridgeSlippage, 1, true, {
value: amountIn2
});
const balAfter = await ethers.provider.getBalance(sender);
expect(balAfter <= balBefore - amountIn2);
const message = encodeMessage(dstSwap, sender.address, expectNonce, true);
const expectId = computeId(sender.address, srcChainId, dstChainId, message);
await expect(tx)
.to.emit(xswap, 'SwapRequestSent')
.withArgs(expectId, dstChainId, amountIn2, srcSwap.path[0], dstSwap.path[1]);
const expectedSendAmt = slip(amountIn2, 5);
const srcXferId = solidityPackedKeccak256(
['address', 'address', 'address', 'uint256', 'uint64', 'uint64', 'uint64'],
[
await xswap.getAddress(),
receiver.address,
await tokenB.getAddress(),
expectedSendAmt,
dstChainId,
expectNonce,
srcChainId
]
);
await expect(tx)
.to.emit(bridge, 'Send')
.withArgs(
srcXferId,
xswap.getAddress(),
receiver.address,
tokenB.getAddress(),
expectedSendAmt,
dstChainId,
expectNonce,
maxBridgeSlippage
);
});
});
describe('Test executeMessageWithTransfer', function () {
beforeEach(async () => {
await prepare();
// impersonate MessageBus as admin to gain access to calling executeMessageWithTransfer
await xswap.connect(admin).setMessageBus(admin.address);
});
const srcChainId = 1;
it('should swap', async function () {
dstSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
const message = encodeMessage(dstSwap, receiver.address, expectNonce, false);
const balB1 = await tokenB.connect(admin).balanceOf(receiver.address);
await tokenA.connect(admin).transfer(xswap.getAddress(), amountIn);
const tx = await xswap
.connect(admin)
.executeMessageWithTransfer(ZeroAddress, tokenA.getAddress(), amountIn, srcChainId, message, ZeroAddress);
const balB2 = await tokenB.connect(admin).balanceOf(receiver.address);
const id = computeId(receiver.address, srcChainId, chainId, message);
const dstAmount = slip(amountIn, 5);
const expectStatus = 1; // SwapStatus.Succeeded
await expect(tx).to.emit(xswap, 'SwapRequestDone').withArgs(id, dstAmount, expectStatus);
expect(balB2).to.equal(balB1 + dstAmount);
});
it('should swap and send native', async function () {
dstSwap.path = [await tokenA.getAddress(), await weth.getAddress()];
const amountIn2 = parseUnits('10');
dstSwap.minRecvAmt = slip(amountIn2, 10);
const message = encodeMessage(dstSwap, receiver.address, expectNonce, true);
const bal1 = await ethers.provider.getBalance(receiver);
await tokenA.connect(admin).transfer(xswap.getAddress(), amountIn2);
const tx = await xswap
.connect(admin)
.executeMessageWithTransfer(ZeroAddress, tokenA.getAddress(), amountIn2, srcChainId, message, ZeroAddress);
const bal2 = await ethers.provider.getBalance(receiver);
const id = computeId(receiver.address, srcChainId, chainId, message);
const dstAmount = slip(amountIn2, 5);
const expectStatus = 1; // SwapStatus.Succeeded
await expect(tx).to.emit(xswap, 'SwapRequestDone').withArgs(id, dstAmount, expectStatus);
expect(bal2 == bal1 + dstAmount);
});
it('should send bridge token to receiver if no dst swap specified', async function () {
dstSwap.path = [await tokenA.getAddress()];
const message = encodeMessage(dstSwap, receiver.address, expectNonce, false);
await tokenA.connect(admin).transfer(xswap.getAddress(), amountIn);
const balA1 = await tokenA.connect(receiver).balanceOf(receiver.address);
const tx = await xswap
.connect(admin)
.executeMessageWithTransfer(ZeroAddress, tokenA.getAddress(), amountIn, srcChainId, message, ZeroAddress);
const balA2 = await tokenA.connect(receiver).balanceOf(receiver.address);
const id = computeId(receiver.address, srcChainId, chainId, message);
const expectStatus = 1; // SwapStatus.Succeeded
await expect(tx).to.emit(xswap, 'SwapRequestDone').withArgs(id, amountIn, expectStatus);
expect(balA2).to.equal(balA1 + amountIn);
});
it('should send bridge token to receiver if swap fails on dst chain', async function () {
srcSwap.path = [await tokenA.getAddress(), await tokenB.getAddress()];
dstSwap.path = [await tokenB.getAddress(), await tokenA.getAddress()];
const bridgeAmount = slip(amountIn, 5);
dstSwap.minRecvAmt = bridgeAmount; // dst chain swap should fail due to slippage
const msg = encodeMessage(dstSwap, receiver.address, expectNonce, false);
const balA1 = await tokenA.balanceOf(receiver.address);
const balB1 = await tokenB.balanceOf(receiver.address);
await tokenB.connect(admin).transfer(xswap.getAddress(), bridgeAmount);
const tx = xswap
.connect(admin)
.executeMessageWithTransfer(ZeroAddress, tokenB.getAddress(), bridgeAmount, srcChainId, msg, ZeroAddress);
const expectId = computeId(receiver.address, srcChainId, chainId, msg);
const expectStatus = 3; // SwapStatus.Fallback
await expect(tx).to.emit(xswap, 'SwapRequestDone').withArgs(expectId, slip(amountIn, 5), expectStatus);
const balA2 = await tokenA.balanceOf(receiver.address);
const balB2 = await tokenB.balanceOf(receiver.address);
expect(balA2, 'balance A after').equals(balA1);
expect(balB2, 'balance B after').equals(balB1 + bridgeAmount);
});
});
================================================
FILE: test/ValidatorSigner.spec.ts
================================================
import { expect } from 'chai';
import { AbiCoder, parseUnits, solidityPackedKeccak256, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { SGN, Staking, TestERC20 } from '../typechain';
import { deployContracts, getAccounts } from './lib/common';
import * as consts from './lib/constants';
describe('Validator Signer Tests', function () {
async function fixture() {
const [admin] = await ethers.getSigners();
const { staking, sgn, celr } = await deployContracts(admin);
return { admin, staking, sgn, celr };
}
const abiCoder = AbiCoder.defaultAbiCoder();
let staking: Staking;
let sgn: SGN;
let celr: TestERC20;
let admin: HardhatEthersSigner;
let validators: Wallet[];
let signers: Wallet[];
beforeEach(async () => {
const res = await loadFixture(fixture);
staking = res.staking;
sgn = res.sgn;
celr = res.celr;
admin = res.admin;
const accounts = await getAccounts(res.admin, [celr], 4);
validators = [accounts[0], accounts[1]];
signers = [accounts[2], accounts[3]];
const stakingAddress = await staking.getAddress();
await celr.connect(validators[0]).approve(stakingAddress, parseUnits('100'));
await celr.connect(validators[1]).approve(stakingAddress, parseUnits('100'));
await staking
.connect(validators[0])
.initializeValidator(signers[0].address, consts.MIN_VALIDATOR_TOKENS, consts.COMMISSION_RATE);
});
it('should fail to initialize a validator using another validator as signer', async function () {
await expect(
staking
.connect(validators[1])
.initializeValidator(validators[0].address, consts.MIN_VALIDATOR_TOKENS, consts.COMMISSION_RATE)
).to.be.revertedWith('Signer is other validator');
});
it('should fail to initialize a validator using a signer being used by another validator', async function () {
await expect(
staking
.connect(validators[1])
.initializeValidator(signers[0].address, consts.MIN_VALIDATOR_TOKENS, consts.COMMISSION_RATE)
).to.be.revertedWith('Signer already used');
});
it('should be able to bond validator using signer address', async function () {
await expect(staking.connect(signers[0]).bondValidator())
.to.emit(staking, 'ValidatorStatusUpdate')
.withArgs(validators[0].address, consts.STATUS_BONDED);
});
it('should update sgn address using signer address', async function () {
const sgnAddr = solidityPackedKeccak256(['string'], ['sgnaddr1']);
await expect(sgn.connect(signers[0]).updateSgnAddr(sgnAddr))
.to.emit(sgn, 'SgnAddrUpdate')
.withArgs(validators[0].address, '0x', sgnAddr)
.to.emit(staking, 'ValidatorNotice')
.withArgs(validators[0].address, 'sgn-addr', sgnAddr, sgn.getAddress());
});
describe('after both validators are bonded', async () => {
beforeEach(async () => {
await staking
.connect(validators[1])
.initializeValidator(signers[1].address, consts.MIN_VALIDATOR_TOKENS, consts.COMMISSION_RATE);
await staking.connect(validators[0]).bondValidator();
await staking.connect(validators[1]).bondValidator();
});
it('should update signer correctly', async function () {
let data = abiCoder.encode(['address'], [admin.address]);
await expect(staking.connect(validators[0]).updateValidatorSigner(admin.address))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validators[0].address, 'signer', data, ZeroAddress);
data = abiCoder.encode(['address'], [validators[0].address]);
await expect(staking.connect(validators[0]).updateValidatorSigner(validators[0].address))
.to.emit(staking, 'ValidatorNotice')
.withArgs(validators[0].address, 'signer', data, ZeroAddress);
});
it('should fail to update signer with invalid inputs', async function () {
await expect(staking.connect(validators[0]).updateValidatorSigner(signers[0].address)).to.be.revertedWith(
'Signer already used'
);
await expect(staking.connect(validators[0]).updateValidatorSigner(signers[1].address)).to.be.revertedWith(
'Signer already used'
);
await expect(staking.connect(validators[0]).updateValidatorSigner(validators[1].address)).to.be.revertedWith(
'Signer is other validator'
);
});
});
});
================================================
FILE: test/lib/common.ts
================================================
import { parseUnits, Wallet, ZeroAddress } from 'ethers';
import { ethers } from 'hardhat';
import {
Bridge,
Bridge__factory,
DummySwap,
DummySwap__factory,
FarmingRewards,
FarmingRewards__factory,
Govern,
Govern__factory,
GovernedOwnerProxy,
GovernedOwnerProxy__factory,
MessageBus,
MessageBus__factory,
MsgTest,
MsgTest__factory,
PeggedTokenBridge,
PeggedTokenBridge__factory,
Sentinel,
Sentinel__factory,
SGN,
SGN__factory,
SimpleGovernance,
SimpleGovernance__factory,
SingleBridgeToken,
SingleBridgeToken__factory,
Staking,
Staking__factory,
StakingReward,
StakingReward__factory,
TestERC20,
TestERC20__factory,
TransferSwap,
TransferSwap__factory,
Viewer,
Viewer__factory,
WETH,
WETH__factory
} from '../../typechain';
import * as consts from './constants';
import type { AbstractSigner, AddressLike, ContractRunner } from 'ethers';
interface DeploymentInfo {
staking: Staking;
sgn: SGN;
stakingReward: StakingReward;
farmingRewards: FarmingRewards;
govern: Govern;
viewer: Viewer;
celr: TestERC20;
}
export async function deployContracts(admin: ContractRunner): Promise {
const testERC20Factory = new TestERC20__factory();
const celr = await testERC20Factory.connect(admin).deploy();
const celrAddress = await celr.getAddress();
const stakingFactory = new Staking__factory();
const staking = await stakingFactory
.connect(admin)
.deploy(
celrAddress,
consts.PROPOSAL_DEPOSIT,
consts.VOTING_PERIOD,
consts.UNBONDING_PERIOD,
consts.MAX_VALIDATOR_NUM,
consts.MIN_VALIDATOR_TOKENS,
consts.MIN_SELF_DELEGATION,
consts.ADVANCE_NOTICE_PERIOD,
consts.VALIDATOR_BOND_INTERVAL,
consts.MAX_SLASH_FACTOR
);
const stakingAddress = await staking.getAddress();
const sgnFactory = new SGN__factory();
const sgn = await sgnFactory.connect(admin).deploy(stakingAddress);
const stakingRewardFactory = new StakingReward__factory();
const stakingReward = await stakingRewardFactory.connect(admin).deploy(stakingAddress);
const stakingRewardAddress = await stakingReward.getAddress();
const farmingRewardsFactory = new FarmingRewards__factory();
const farmingRewards = await farmingRewardsFactory.connect(admin).deploy(stakingAddress);
const governFactory = new Govern__factory();
const govern = await governFactory.connect(admin).deploy(stakingAddress, celrAddress, stakingRewardAddress);
const viewerFactory = new Viewer__factory();
const viewer = await viewerFactory.connect(admin).deploy(stakingAddress);
return { staking, sgn, stakingReward, farmingRewards, govern, viewer, celr };
}
interface BridgeInfo {
bridge: Bridge;
token: TestERC20;
pegBridge: PeggedTokenBridge;
pegToken: SingleBridgeToken;
}
export async function deployBridgeContracts(admin: ContractRunner): Promise {
const testERC20Factory = new TestERC20__factory();
const token = await testERC20Factory.connect(admin).deploy();
await token.waitForDeployment();
const bridgeFactory = new Bridge__factory();
const bridge = await bridgeFactory.connect(admin).deploy();
await bridge.waitForDeployment();
const pegBridgeFactory = new PeggedTokenBridge__factory();
const pegBridge = await pegBridgeFactory.connect(admin).deploy(bridge.getAddress());
await pegBridge.waitForDeployment();
const pegTokenFactory = new SingleBridgeToken__factory();
const pegToken = await pegTokenFactory.connect(admin).deploy('PegToken', 'PGT', 18, pegBridge.getAddress());
await pegToken.waitForDeployment();
return { bridge, token, pegBridge, pegToken };
}
interface MessageInfo {
bridge: Bridge;
msgBus: MessageBus;
msgTest: MsgTest;
token: TestERC20;
}
export async function deployMessageContracts(admin: ContractRunner): Promise {
const testERC20Factory = new TestERC20__factory();
const token = await testERC20Factory.connect(admin).deploy();
const bridgeFactory = new Bridge__factory();
const bridge = await bridgeFactory.connect(admin).deploy();
const bridgeAddress = await bridge.getAddress();
const msgBusFactory = new MessageBus__factory();
const msgBus = await msgBusFactory
.connect(admin)
.deploy(bridgeAddress, bridgeAddress, ZeroAddress, ZeroAddress, ZeroAddress, ZeroAddress);
const msgTestFactory = new MsgTest__factory();
const msgTest = await msgTestFactory.connect(admin).deploy(msgBus.getAddress());
return { bridge, msgBus, msgTest, token };
}
interface GovernedOwnerInfo {
gov: SimpleGovernance;
proxy: GovernedOwnerProxy;
}
export async function deployGovernedOwner(
admin: ContractRunner & AddressLike,
initVoterNum: number
): Promise {
const proxyFactory = new GovernedOwnerProxy__factory();
const proxy = await proxyFactory.connect(admin).deploy(admin);
const voters: string[] = [];
const powers: number[] = [];
for (let i = 0; i < initVoterNum; i++) {
const voter = new Wallet(consts.userPrivKeys[i]).connect(ethers.provider);
voters.push(voter.address);
powers.push(100);
}
const govFactory = new SimpleGovernance__factory();
const gov = await govFactory.connect(admin).deploy(voters, powers, [proxy.getAddress()], 3600, 60, 40);
await proxy.initGov(gov.getAddress());
return { gov, proxy };
}
export async function deploySentinel(admin: ContractRunner): Promise {
const factory = new Sentinel__factory();
const sentinel = await factory.connect(admin).deploy([], [], []);
return sentinel;
}
interface SwapInfo {
transferSwap: TransferSwap;
tokenA: TestERC20;
tokenB: TestERC20;
bridge: Bridge;
swap: DummySwap;
weth: WETH;
}
export async function deploySwapContracts(admin: ContractRunner): Promise {
const testERC20FactoryA = new TestERC20__factory();
const tokenA = await testERC20FactoryA.connect(admin).deploy();
const testERC20FactoryB = new TestERC20__factory();
const tokenB = await testERC20FactoryB.connect(admin).deploy();
const bridgeFactory = new Bridge__factory();
const bridge = await bridgeFactory.connect(admin).deploy();
const bridgeAddress = await bridge.getAddress();
const busFactory = new MessageBus__factory();
const bus = await busFactory
.connect(admin)
.deploy(bridgeAddress, bridgeAddress, ZeroAddress, ZeroAddress, ZeroAddress, ZeroAddress);
const swapFactory = new DummySwap__factory();
const swap = await swapFactory.connect(admin).deploy(parseUnits('5')); // 5% fixed fake slippage
const wethFactory = new WETH__factory();
const weth = await wethFactory.connect(admin).deploy();
const transferSwapFactory = new TransferSwap__factory();
const transferSwap = await transferSwapFactory
.connect(admin)
.deploy(bus.getAddress(), swap.getAddress(), weth.getAddress());
return { tokenA, tokenB, transferSwap, swap, bridge, weth };
}
export async function getAccounts(admin: AbstractSigner, assets: TestERC20[], num: number): Promise {
const accounts: Wallet[] = [];
for (let i = 0; i < num; i++) {
accounts.push(new ethers.Wallet(consts.userPrivKeys[i]).connect(ethers.provider));
await admin.sendTransaction({
to: accounts[i].address,
value: parseUnits('11')
});
for (let j = 0; j < assets.length; j++) {
await assets[j].transfer(accounts[i].address, parseUnits('1000'));
}
}
accounts.sort((a, b) => (a.address.toLowerCase() > b.address.toLowerCase() ? 1 : -1));
return accounts;
}
export async function advanceBlockNumber(blkNum: number): Promise {
const promises = [];
for (let i = 0; i < blkNum; i++) {
promises.push(ethers.provider.send('evm_mine', []));
}
await Promise.all(promises);
}
export async function advanceBlockNumberTo(target: number): Promise {
const blockNumber = await ethers.provider.getBlockNumber();
const promises = [];
for (let i = blockNumber; i < target; i++) {
promises.push(ethers.provider.send('evm_mine', []));
}
await Promise.all(promises);
}
export async function getBlockTime() {
const blockNumber = await ethers.provider.getBlockNumber();
const block = await ethers.provider.getBlock(blockNumber);
if (block) {
return block.timestamp;
}
throw Error('invalid block number');
}
export async function advanceBlockTime(blkTime: number) {
const currBlkTime = await getBlockTime();
await ethers.provider.send('evm_setNextBlockTimestamp', [currBlkTime + blkTime]);
await ethers.provider.send('evm_mine', []);
}
export function getAddrs(signers: Wallet[]) {
const addrs: string[] = [];
for (let i = 0; i < signers.length; i++) {
addrs.push(signers[i].address);
}
return addrs;
}
================================================
FILE: test/lib/constants.ts
================================================
import { parseUnits } from 'ethers';
export const PROPOSAL_DEPOSIT = 100;
export const VOTING_PERIOD = 20;
export const UNBONDING_PERIOD = 50;
export const MAX_VALIDATOR_NUM = 7;
export const MIN_VALIDATOR_TOKENS = parseUnits('4');
export const ADVANCE_NOTICE_PERIOD = 10;
export const VALIDATOR_BOND_INTERVAL = 0;
export const MAX_SLASH_FACTOR = 1e5; // 10%
export const MIN_SELF_DELEGATION = parseUnits('2');
export const VALIDATOR_STAKE = parseUnits('1'); // smaller than MIN_VALIDATOR_TOKENS for testing purpose
export const DELEGATOR_STAKE = parseUnits('6');
export const COMMISSION_RATE = 100;
export const SLASH_FACTOR = 50000; // 5%
export const STATUS_UNBONDED = 1;
export const STATUS_UNBONDING = 2;
export const STATUS_BONDED = 3;
export const ENUM_PROPOSAL_DEPOSIT = 0;
export const ENUM_VOTING_PERIOD = 1;
export const ENUM_UNBONDING_PERIOD = 2;
export const ENUM_MAX_VALIDATOR_NUM = 3;
export const ENUM_MIN_VALIDATOR_TOKENS = 4;
export const ENUM_MIN_SELF_DELEGATION = 5;
export const ENUM_ADVANCE_NOTICE_PERIOD = 6;
export const ENUM_VOTE_OPTION_UNVOTED = 0;
export const ENUM_VOTE_OPTION_YES = 1;
export const ENUM_VOTE_OPTION_ABSTAIN = 2;
export const ENUM_VOTE_OPTION_NO = 3;
export const SUB_FEE = parseUnits('100000000', 'wei');
export const TYPE_MSG_XFER = 0;
export const TYPE_MSG_ONLY = 1;
export const MSG_TX_NULL = 0;
export const MSG_TX_SUCCESS = 1;
export const MSG_TX_FAIL = 2;
export const MSG_TX_FALLBACK = 3;
export const XFER_TYPE_LQ_RELAY = 1;
export const XFER_TYPE_LQ_WITHDRAW = 2;
export const XFER_TYPE_PEG_MINT = 3;
export const XFER_TYPE_PEG_WITHDRAW = 4;
export const XFER_TYPE_PEGV2_MINT = 5;
export const XFER_TYPE_PEGV2_WITHDRAW = 6;
export const GovExternalDefault = 0;
export const GovExternalFastPass = 1;
export const GovInternalParamChange = 2;
export const GovInternalVoterUpdate = 3;
export const GovInternalProxyUpdate = 4;
export const GovInternalTokenTransfer = 5;
export const GovParamActivePeriod = 0;
export const GovParamQuorumThreshold = 1;
export const GovParamFastPassThreshold = 2;
export const userPrivKeys = [
'0x36f2243a51a0f879b1859fff1a663ac04aeebca1bcff4d7dc5a8b38e53211199',
'0xc0bf10873ddb6d554838f5e4f0c000e85d3307754151add9813ff331b746390d',
'0x68888cc706520c4d5049d38933e0b502e2863781d75de09c499cf0e4e00ba2de',
'0x400e64f3b8fe65ecda0bad60627c41fa607172cf0970fbe2551d6d923fd82f78',
'0xab4c840e48b11840f923a371ba453e4d8884fd23eee1b579f5a3910c9b00a4b6',
'0x0168ea2aa71023864b1c8eb65997996d726e5068c12b20dea81076ef56380465',
'0xd3733feb467076219337afed04787c48f5aa23c9c998bee1dc7742d12f7628e9',
'0xf9d76b9beacb01e431440a2750cb8aec04b782dfa7a7ef62584550e5db7347d6',
'0xecdd83652d7fddaffa0993f7a7d58d423a76737d7a90e81d06a32aecb8d470db',
'0xf37790438d0d8108a5bc588f727b84f041e355e825bbb8b36a6c83efa9ad3176',
'0x212f9e9d305ecd326ef88da498cd869f2e3ff2909f315f88f563594da8663990',
'0xdf0c45cfa93acf88ed0becd7a41df47e42217c0febc2be4aa7126decbccdc887',
'0x33209d3c38365492473417af8b03e3e6d3eb4f6269fe33f47edba62882732dbb',
'0xfe964eb524f182ea08aa2528fc5ec660238908fe77a5dca5f2954388bbe2cfc8',
'0x03abc71ae40f50de995e97421d28394818352ac91de757411293ec1177563806',
'0xce86dac0655a8822db84e505c1fdc36410d7e98ac227a9671f309d9dae2c741f',
'0xbb08d5f77da4a71cefa19f254e347caa0c837d2639d10c8a2bf37aea75c97d15',
'0x6b2e4b681206a0abca47ad72f194158c18f1a755b96d1fd8797baaec870f6000',
'0x1f7ede2316ee0423bb38127c6a1fc4d87f5804460757ffb6c3249335e3f2ecb4',
'0x0f072d61ec1b47e8f09c20e935d942c291d3229932ab1560cc567e32c2f87e94',
'0xee15c0525cc1f3292e9905d36f78a9ad48551e365174e351ad867c0bb4f54d9e'
];
================================================
FILE: test/lib/proto.ts
================================================
import { expect } from 'chai';
import {
AbstractSigner,
BigNumberish,
getBytes,
solidityPacked,
solidityPackedKeccak256,
toBeArray,
Wallet
} from 'ethers';
import protobuf from 'protobufjs';
import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
protobuf.common('google/protobuf/descriptor.proto', {});
interface Proto {
Slash: protobuf.Type;
StakingReward: protobuf.Type;
FarmingRewards: protobuf.Type;
AcctAmtPair: protobuf.Type;
Relay: protobuf.Type;
WithdrawMsg: protobuf.Type;
Mint: protobuf.Type;
}
async function getProtos(): Promise {
const staking = await protobuf.load(`${__dirname}/../../contracts/libraries/proto/staking.proto`);
const farming = await protobuf.load(`${__dirname}/../../contracts/libraries/proto/farming.proto`);
const bridge = await protobuf.load(`${__dirname}/../../contracts/libraries/proto/bridge.proto`);
const pool = await protobuf.load(`${__dirname}/../../contracts/libraries/proto/pool.proto`);
const pegged = await protobuf.load(`${__dirname}/../../contracts/libraries/proto/pegged.proto`);
const Slash = staking.lookupType('staking.Slash');
const StakingReward = staking.lookupType('staking.StakingReward');
const FarmingRewards = farming.lookupType('farming.FarmingRewards');
const AcctAmtPair = staking.lookupType('staking.AcctAmtPair');
const Relay = bridge.lookupType('bridge.Relay');
const WithdrawMsg = pool.lookupType('pool.WithdrawMsg');
const Mint = pegged.lookupType('pegged.Mint');
return {
Slash,
StakingReward,
FarmingRewards,
AcctAmtPair,
Relay,
WithdrawMsg,
Mint
};
}
export async function calculateSignatures(signers: AbstractSigner[], hash: Uint8Array): Promise {
const sigs = [];
for (let i = 0; i < signers.length; i++) {
const sig = await signers[i].signMessage(hash);
sigs.push(sig);
}
return sigs;
}
export async function getStakingRewardRequest(
recipient: string,
cumulativeRewardAmount: BigNumberish,
signers: Wallet[],
chainId: number,
contractAddress: string
): Promise<{ rewardBytes: Uint8Array; sigs: string[] }> {
const { StakingReward } = await getProtos();
const reward = {
recipient: getBytes(recipient),
cumulativeRewardAmount: toBeArray(cumulativeRewardAmount)
};
const rewardProto = StakingReward.create(reward);
const rewardBytes = StakingReward.encode(rewardProto).finish();
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, contractAddress, 'StakingReward']);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, rewardBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { rewardBytes, sigs };
}
export async function getFarmingRewardsRequest(
recipient: string,
tokenAddresses: string[],
cumulativeRewardAmounts: BigNumberish[],
signers: Wallet[],
chainId: number,
contractAddress: string
): Promise<{ rewardBytes: Uint8Array; sigs: string[] }> {
const { FarmingRewards } = await getProtos();
const reward = {
recipient: getBytes(recipient),
tokenAddresses: tokenAddresses.map((addr) => getBytes(addr)),
cumulativeRewardAmounts: cumulativeRewardAmounts.map(toBeArray)
};
const rewardProto = FarmingRewards.create(reward);
const rewardBytes = FarmingRewards.encode(rewardProto).finish();
const domain = solidityPackedKeccak256(
['uint256', 'address', 'string'],
[chainId, contractAddress, 'FarmingRewards']
);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, rewardBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { rewardBytes, sigs };
}
async function getAcctAmtPairs(accounts: string[], amounts: bigint[]): Promise {
const { AcctAmtPair } = await getProtos();
expect(accounts.length).to.equal(amounts.length);
const pairs = [];
for (let i = 0; i < accounts.length; i++) {
const pair = {
account: getBytes(accounts[i]),
amount: toBeArray(amounts[i])
};
const pairProto = AcctAmtPair.create(pair);
pairs.push(pairProto);
}
return pairs;
}
export async function getSlashRequest(
validatorAddr: string,
nonce: number,
slashFactor: number,
expireTime: number,
jailPeriod: number,
collectorAddrs: string[],
collectorAmts: bigint[],
signers: Wallet[],
chainId: number,
contractAddress: string
): Promise<{ slashBytes: Uint8Array; sigs: string[] }> {
const { Slash } = await getProtos();
const collectors = await getAcctAmtPairs(collectorAddrs, collectorAmts);
const slash = {
validator: getBytes(validatorAddr),
nonce,
slashFactor,
expireTime,
jailPeriod,
collectors
};
const slashProto = Slash.create(slash);
const slashBytes = Slash.encode(slashProto).finish();
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, contractAddress, 'Slash']);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, slashBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { slashBytes, sigs };
}
export async function getRelayRequest(
sender: string,
receiver: string,
token: string,
amount: bigint,
srcChainId: number,
dstChainId: number,
srcTransferId: string,
signers: (Wallet | SignerWithAddress)[],
contractAddress: string
): Promise<{ relayBytes: Uint8Array; sigs: string[] }> {
const { Relay } = await getProtos();
const relay = {
sender: getBytes(sender),
receiver: getBytes(receiver),
token: getBytes(token),
amount: toBeArray(amount),
srcChainId,
dstChainId,
srcTransferId: getBytes(srcTransferId)
};
const relayProto = Relay.create(relay);
const relayBytes = Relay.encode(relayProto).finish();
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [dstChainId, contractAddress, 'Relay']);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, relayBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const signerAddrs = [];
for (let i = 0; i < signers.length; i++) {
signerAddrs.push(signers[i].address);
}
signers.sort((a, b) => (a.address.toLowerCase() > b.address.toLowerCase() ? 1 : -1));
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { relayBytes, sigs };
}
export async function getWithdrawRequest(
chainId: number,
seqnum: number,
receiver: string,
token: string,
amount: bigint,
refid: string,
signers: Wallet[],
contractAddress: string
): Promise<{ withdrawBytes: Uint8Array; sigs: string[] }> {
const { WithdrawMsg } = await getProtos();
const withdraw = {
chainid: chainId,
seqnum: seqnum,
receiver: getBytes(receiver),
token: getBytes(token),
amount: toBeArray(amount),
refid: getBytes(refid)
};
const withdrawProto = WithdrawMsg.create(withdraw);
const withdrawBytes = WithdrawMsg.encode(withdrawProto).finish();
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, contractAddress, 'WithdrawMsg']);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, withdrawBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const signerAddrs = [];
for (let i = 0; i < signers.length; i++) {
signerAddrs.push(signers[i].address);
}
signers.sort((a, b) => (a.address.toLowerCase() > b.address.toLowerCase() ? 1 : -1));
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { withdrawBytes, sigs };
}
export async function getMintRequest(
token: string,
account: string,
amount: bigint,
depositor: string,
refChainId: number,
refId: string,
signers: Wallet[],
chainId: number,
contractAddress: string
): Promise<{ mintBytes: Uint8Array; sigs: string[] }> {
const { Mint } = await getProtos();
const mint = {
token: getBytes(token),
account: getBytes(account),
amount: toBeArray(amount),
depositor: getBytes(depositor),
refChainId,
refId: getBytes(refId)
};
const mintProto = Mint.create(mint);
const mintBytes = Mint.encode(mintProto).finish();
const domain = solidityPackedKeccak256(['uint256', 'address', 'string'], [chainId, contractAddress, 'Mint']);
const signedData = solidityPacked(['bytes32', 'bytes'], [domain, mintBytes]);
const signedDataHash = solidityPackedKeccak256(['bytes'], [signedData]);
const signerAddrs = [];
for (let i = 0; i < signers.length; i++) {
signerAddrs.push(signers[i].address);
}
signers.sort((a, b) => (a.address.toLowerCase() > b.address.toLowerCase() ? 1 : -1));
const sigs = await calculateSignatures(signers, getBytes(signedDataHash));
return { mintBytes, sigs };
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es5", "es6"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"outDir": "dist",
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "es2020"
},
"exclude": ["artifacts", "build", "cache", "node_modules"],
"include": [
"deploy/**/*",
"scripts/**/*",
"test/**/*",
"typechain/**/*",
"types/**/*",
"hardhat.config.ts"
]
}