Wrapped Token Development
Wrapped token represents another asset in 1:1 ratio. WETH is ETH wrapped in ERC-20 interface. WBTC is Bitcoin on Ethereum, backed by real BTC at custodian. wstETH is stETH in non-rebasing form. Behind simple idea lie several fundamentally different architectural models with very different security guarantees.
Three wrapped token models
Custodial wrap (WBTC-model). Original asset stored at centralized custodian (BitGo for WBTC). When user deposits BTC — accredited minter creates WBTC on-chain. On redemption — custodian issues BTC. Contract is simple, risk is trust in custodian. BitGo holds ~150K BTC ($9B+). Single point of failure.
Smart contract wrap (WETH-model). Smart contract is custodian. User sends ETH → gets WETH 1:1. On unwrap — returns WETH → gets ETH. No trust in third party, full reserve transparency on-chain. Works only if both assets in same blockchain.
Cross-chain wrap with bridge. Asset locked on one chain, wrapped version minted on other. Most complex and risky. Bridges are biggest source of DeFi hacks (Ronin $625M, Wormhole $320M, Nomad $190M).
WETH contract: implementation details
WETH9 is one of most copied contracts in Ethereum. Original in production since 2017 and contains ~50 lines of code. But nuances exist:
contract WETH9 {
string public name = "Wrapped Ether";
string public symbol = "WETH";
uint8 public decimals = 18;
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);
// Wrapper via fallback — deposit on direct ETH send
receive() external payable {
deposit();
}
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
payable(msg.sender).transfer(wad);
emit Withdrawal(msg.sender, wad);
}
function totalSupply() public view returns (uint) {
return address(this).balance;
}
// ... ERC20 transfer/approve/transferFrom
}
Contract invariant: address(this).balance == totalSupply() always. This makes WETH trusted: reserves verifiable on-chain in real-time.
Important difference from regular ERC-20: totalSupply() computed as address(this).balance, not stored separately. Guarantees synchronization, but means approve + transferFrom for ETH impossible without WETH (hence its necessity for DeFi protocols).
Cross-chain wrapped token with lock-and-mint
For token that should exist in multiple chains (e.g., your ERC-20 token on Ethereum and equivalent on BSC):
Lock contract on source chain (Ethereum):
contract TokenBridge {
IERC20 public immutable token;
address public immutable relayer; // trusted or decentralized
mapping(bytes32 => bool) public processedMessages;
event TokensLocked(
address indexed sender,
uint256 amount,
uint256 destinationChainId,
address destinationAddress,
bytes32 messageId
);
function lock(
uint256 amount,
uint256 destinationChainId,
address destinationAddress
) external {
require(amount > 0, "Zero amount");
token.safeTransferFrom(msg.sender, address(this), amount);
bytes32 messageId = keccak256(
abi.encodePacked(msg.sender, amount, destinationChainId, destinationAddress, block.timestamp)
);
emit TokensLocked(msg.sender, amount, destinationChainId, destinationAddress, messageId);
}
// Relayer calls unlock on destination chain burn
function unlock(
address recipient,
uint256 amount,
bytes32 messageId,
bytes calldata relayerSignature
) external {
require(!processedMessages[messageId], "Already processed");
require(verifyRelayerSignature(recipient, amount, messageId, relayerSignature), "Invalid signature");
processedMessages[messageId] = true;
token.safeTransfer(recipient, amount);
}
}
Wrapped contract on destination chain (BSC):
contract WrappedToken is ERC20, Ownable {
address public immutable bridge;
constructor(string memory name, string memory symbol, address _bridge)
ERC20(name, symbol) Ownable(msg.sender)
{
bridge = _bridge;
}
function mint(address to, uint256 amount) external {
require(msg.sender == bridge, "Only bridge");
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
require(msg.sender == bridge, "Only bridge");
_burn(from, amount);
}
}
Relayer: centralized vs decentralized
Most critical part of cross-chain bridge — who and how verifies events on other chain.
Centralized relayer — your server listens to events on source chain and calls functions on destination chain. Simple to develop, fast, but centralized point of failure. If server compromised — attacker can create infinite mints without real lock.
Multisig relayers — N independent operators must sign each message, contract checks signature threshold. Used by Multichain (before hack), deBridge. Safer but harder to orchestrate.
Decentralized messaging (LayerZero, Chainlink CCIP, Wormhole) — use existing verified infrastructure instead of own relayers. LayerZero: Ultra Light Node verifies block headers via on-chain Light Client + Oracle for finality. Reduces trust assumptions, adds provider dependency.
// LayerZero integration
import "@layerzerolabs/lz-evm-sdk-v2/contracts/oft/OFT.sol";
contract MyToken is OFT {
constructor(
string memory name,
string memory symbol,
address lzEndpoint,
address owner
) OFT(name, symbol, lzEndpoint, owner) {}
// OFT standard automatically implements cross-chain transfer
// via burn on source + mint on destination through LayerZero messaging
}
OFT (Omnichain Fungible Token) from LayerZero is ready standard for cross-chain tokens with minimal custom code.
Proof of reserves
For custodial wrapped tokens — public reserve verifiability critical after BUSD collapse and other centralized stablecoin failures.
Chainlink Proof of Reserve — oracle verifying off-chain reserves (e.g., BTC at custodian) and publishing on-chain. Contract can check reserves before each mint:
AggregatorV3Interface public reserveFeed;
function mint(address to, uint256 amount) external onlyMinter {
(, int256 reserveBalance,,,) = reserveFeed.latestRoundData();
require(
int256(totalSupply() + amount) <= reserveBalance,
"Insufficient reserves"
);
_mint(to, amount);
}
Timeline and stack
| Wrapped token type | Complexity | Timeline |
|---|---|---|
| WETH-style (same chain) | Low | 1–2 days |
| Cross-chain with centralized relayer | Medium | 1–2 weeks |
| Cross-chain via LayerZero/CCIP | Medium | 1 week + integration testing |
| Custom decentralized bridge | High | 6–12 weeks + audit |
For cross-chain tokens with real assets — audit mandatory. Bridges are most attacked component of all DeFi.







