UpgradeableTokens
1. Glossary
wrappedToken: ERC20 standard token created by the bridgeupgradeableWrappedToken: TransparentProxy pointing to a ERC20 standard upgradeable implementation
2. Rationale
2.1 Current scenario
Wrapped tokens are created by the bridge using the create2 opcode and it creates a simple and standard ERC20 token.
Note that current deployed chains use this approach to create wrappedTokens and wrapped tokens have the same addresses in all chains deployed.
This approach does not imply any security assumption beyond the address that is able to upgrade the bridge (normally a timelock).
2.2 Current issue
On SovereignChains, a new approach to allow more features on the wrapped Tokens has been implemented: custom mappings. This feature currently works given the featured wanted: extend the wrappedToken features.
This feature is purely managed by the bridgeManager meaning that this feature is chain-centric (instead of asset-centric). Asset-centric means that the owner of the asset could deploy its own wrappedToken (or similarly, deploy an upgradeable wrappedToken and give the ownership to an address controlled by the asset).
An asset-centric feature could not be applied on top of the custom mappings implementation. Therefore a new approach needs to be specified in order to allow future version to be asset-centric.
Besides, this approach implies another risky concern about users being able to stop the PP of the chain if LBT reaches a negative balance (this assumption purely depends on the approach on how to use the customMappings. Example: Katana). The implementation of the LBT at SC level provides guarantees that the PP will not be blocked, but just the user claim. NOTE that this affects customMappings and upgradeableWrappedTokens.
2.3 Solving the issue
Deploy transparent proxy which has a ERC20 upgradeable implementation
proxiedTokensManageris the owner of the proxy admin of the proxyproxiedTokensManagercan delegate ownership to any address later onproxiedTokensManageris set at initialization of sovereign bridge from initialization inputs. In case of L1, bridgeV2,proxiedTokensManageris set from the proxy owner of the proxy admin of the bridge itself. The role can be transferred with a two steps procedure.
Security assumptions:
SovereignBridgeis a proxy controlled by a certain address. Managing the tokens from the same address has the same security assumptions.- LBT implementation avoids chain DoS on the PP, more specifically on the LBT with negative value given that an asset could
minttokens as its wish - mainnet bridge to produce the same addresses once it is upgraded
Custom mappings:
- keep functionality on SovereignBridges
- theoretically, functionality will not be used and therefore removed in future versions
⚠ 💡 Already deployed SovereignBridges can have an extra functionality which is deploying the new
upgradeableWrappedTokens. Then, a customMapping could be done to override thewrappedTokenaddress. CustomMappings provides the ability to migrate tokens if used correctly.If the
upgradeableWrappedTokensmints outside the bridge could potentially unbalance the liquidity and/or steal user's assets
3. Requirements
- Deterministic addresses on all chains
- Mainnet bridge and SovereignBridges to deploy new
upgradeableWrappedTokens upgradeableWrappedTokenspointing to a ERC20 standard upgradeable implementationupgradeableWrappedTokensto setbridgeManageras its initial owner- keep
customMappingsfeature
4. Cases
4.1 New SovereignBridges deployed
All the tokens created by the SovereignBridge will have the new addresses and will be upgradeable ERC20 tokens.
Mainnet bridge will also produce the same addresses once the bridge is upgraded.
⚠ Previous tokens deployed by the bridge will still have the old addresses and will not be upgraded. therefore, old address will have all the token liquidity. There will not be liquidity fragmentation.
💡 A mechanism to upgrade previous tokens has been introduced
deployWrappedTokenAndRemap.Function to deploy an upgradeable wrapped token without having to claim asset. It is used to upgrade legacy tokens to the new upgradeable token. After deploying the token it is remapped to be the new functional wtoken. This function can only be called once for each originNetwork/originTokenAddress pair because it deploys a deterministic contract with create2
All tokens created by the bridge will effectively not have the same address as previous wrappedToken versions.
4.2 Previous bridges deployed
Bridges will be upgraded to start using upgradeableWrappedTokens.
All previous tokens deployed on those bridges will have the same address as before and same address will be used.
New tokens will have the new address produced by the upgradeableWrappedToken.
4.3 Chains with no PP (zkEVM / Validium / Mainnet)
Chains with NO PP proofs cannot use this feature since all bridge assets can be stolen, not just the chain's ones.
Mainnet could be a special case where the owner of the upgradeableWrappedTokens is the securityCouncil
securityCouncilis the same address that could potentially halt the bridge andzkEVMMultisigto perform an upgrade with delay 0 afterwardstherefore, seems more rational to setup the timelock as the owner of all
upgradeableWrappedTokensTimelock is not accessible from the Bridge. Therefore, adding a role
bridgeManagerand initialize it with the timelock fits better. Later on, bridgeManager can be initialized as the securityCouncil address or the timelock.
5. Specs
5.1 Deterministic address specification
In order to get a deterministic address when deploying a ERC20 upgradeable token we need to make the create2 parameters to be only dependant on the token information. The rest of the parameters must be equal across all the chains.
The common data shared among all the bridges deployed is its address.
5.2 Previous knowledge
create2 formula to generate addresses:
address = keccak256(0xFF ++ deployer ++ salt ++ keccak256(init_code))[12:]
_logic: implementation address
- initialOwner: owner of the proxy
- _data: typically to initialize storage proxy
5.3 Create2 params TransparentProxy
5.3.1 deployer
The deployer will be always the bridge address when the proxy is deployed
5.3.2 salt
The TokenInfoHash will be set as a salt since it is the unique identifier of a given token.
Note that in previous bridge versions, not only the
TokenInfoHashchanged the address deployed but also the token metadata since it was appended to the initBytecode to be able to setup the constructor.
5.3.4 init_code
initBytecode proxyInitBytecode # constructorArgs:
/// @dev A bytecode stored on chain is used to deploy the proxy in a way that ALWAYS it's used the same
/// bytecode, therefore the proxy addresses are the same in all chains as they are deployed deterministically with same init bytecode
/// @dev there is no constructor args as the implementation address + owner of the proxied are set at constructor level and taken from the bridge itself
bytes memory proxyInitBytecode = abi.encodePacked(
INIT_BYTECODE_TRANSPARENT_PROXY()
);
_logic: implementation addressinitialOwner: owner of the proxy_data: typically to initialize storage proxy Therefore, all above parameters must be the same in all chains in order to get the same address.
5.4 Implementation
- When deploying the new bridge implementation
- deploy proxy init bytecode and store it on an immutable address
- deploy erc20 upgradeable implementation and store it on an immutable address
- When deploying a new upgradeableWrappedToken
- Deploy TokenWrappedTransparentProxy with create2
- The constructor has been modified to get the params from the msg.sender (the bridge). This way, we are not sending constructor args and the init code it's no dependant on constructor and the address is determined byt the bytecode.
constructor() payable ERC1967Proxy( IPolygonZkEVMBridgeV2(msg.sender) .getWrappedTokenBridgeImplementation(), new bytes(0) ) { // Get bridge interface to retrieve proxied tokens manager role _changeAdmin( IPolygonZkEVMBridgeV2(msg.sender).getProxiedTokensManager() ); }
- The constructor has been modified to get the params from the msg.sender (the bridge). This way, we are not sending constructor args and the init code it's no dependant on constructor and the address is determined byt the bytecode.
- attach
initBytecodeProxywith params// 'proxyBytecodeStorer' is the address that contains the initBytecode bytes memory initBytecode = abi.encodePacked( IProxyInitCode(wrappedTokenBytecodeStorer).PROXY_INIT_BYTECODE();, constructorArgsProxy ); - create2 opcode
/// @solidity memory-safe-assembly assembly { newWrappedTokenProxy := create2( 0, add(initBytecode, 0x20), mload(initBytecode), tokenInfoHash ) } - initialize proxy contract
newWrappedTokenProxy.initialize(name, symbol, decimals);
- Deploy TokenWrappedTransparentProxy with create2