How we designed our smart contracts to protect player funds, even from ourselves.
Introduction: The Trust Problem in DeFi
In the world of decentralized finance (DeFi) and Web3 gaming, trust is the most valuable asset. We've all heard the horror stories: projects that vanish overnight, smart contract bugs that lock up user funds forever, and "rug pulls" where developers with privileged access drain the treasury. This creates a climate of fear that stifles innovation and scares away new users.
At Musical Chairs, a simple, fast, and transparently fair on-chain game, we believe that building trust isn't about making promises; it's about writing code that makes those promises unbreakable.
This article isn't just about our game. It's a deep dive into our design philosophy: Trust Through Transparency. We'll walk you through the specific, on-chain mechanisms we've implemented to safeguard player funds, focusing on the evolution of our emergencyWithdrawal function from a powerful tool into a transparent, community-verifiable process.
Section 1: The Double-Edged Sword of "Owner" Power
Any responsible smart contract needs a mechanism for maintenance and upgrades. An "owner" role is often necessary to deploy critical bug fixes or roll out new features. But this power is a double-edged sword. A malicious owner with instant access to critical functions poses the single greatest threat to a project's integrity.
Our first version of the contract (MusicalChairs.sol) included a function for emergencies:
solidity
Show full code block
// From MusicalChairs.sol (V1)
function emergencyWithdrawETH() external virtual onlyOwner nonReentrant {
uint256 balance = address(this).balance;
if (balance == 0) revert NoETHToWithdraw();
(bool sent, ) = owner().call{value: balance}("");
if (!sent) revert ETHEmergencyWithdrawalFailed();
}
This function does its job—it allows the owner to recover all ETH from the contract in a catastrophe. But it has one major flaw in the context of user trust: it's instant. A user would have no time to react. We knew we could do better.
Section 2: Our Solution — The Timelock as a Core Principle
Before we even tackled the emergency withdrawal, we established a core security principle for all critical administrative actions: the Timelock. A timelock is a two-step process where an action is first publicly proposed and can only be executed after a predefined delay has passed.
This is built into our contract's DNA. For example, changing the contract owner isn't a single transaction. It's a public, two-step process governed by a 7-day waiting period.
solidity
Show full code block
// From MusicalChairs.sol
uint256 public constant DEFAULT_TIMELOCK_DELAY = 7 days;
function proposeNewOwner(address newOwnerCandidate) external virtual onlyOwner {
// ... checks ...
proposedNewOwner = newOwnerCandidate;
ownerChangeProposalTimestamp = block.timestamp;
emit OwnershipTransferProposed(newOwnerCandidate, block.timestamp + DEFAULT_TIMELOCK_DELAY);
}
function executeOwnerChange() external virtual {
// ... checks ...
if (block.timestamp < ownerChangeProposalTimestamp + DEFAULT_TIMELOCK_DELAY) revert TimelockNotPassed();
_transferOwnership(proposedNewOwner);
// ...
}
This same timelock pattern is applied to changing the commission wallet and, most importantly, to upgrading the contract itself via our UUPS proxy. No critical change can happen without a 7-day public notice on the blockchain.
Section 3: The Evolution of Safety — emergencyWithdrawal V2
With the timelock philosophy established, it was time to apply it to the emergencyWithdrawal function. In our MusicalChairsGameV2 contract, we completely overhauled this mechanism.
First, we permanently disabled the old, instant function. Calling it now will always fail.
solidity
// From MusicalChairsGameV2.sol
function emergencyWithdrawETH() public pure override {
revert EmergencyWithdrawalDeprecated();
}
Next, we replaced it with a new, transparent, two-step timelocked process:
Step 1: Propose the Withdrawal The owner must first publicly declare their intent to withdraw funds. This function captures the entire contract balance at that moment and records a timestamp.
solidity
Show full code block
// From MusicalChairsGameV2.sol
function proposeEmergencyWithdrawal() external virtual onlyOwner {
uint256 balance = address(this).balance;
if (emergencyWithdrawalProposalTimestamp != 0) revert EmergencyWithdrawalAlreadyProposed();
if (balance == 0) revert NoETHToWithdraw();
proposedEmergencyWithdrawalAmount = balance;
emergencyWithdrawalProposalTimestamp = block.timestamp;
emit EmergencyWithdrawalProposed(balance, block.timestamp + DEFAULT_TIMELOCK_DELAY);
}
Step 2: Execute the Withdrawal (After 7 Days) The funds can only be moved after the 7-day timelock has passed. If the owner tries to execute it even one second early, the transaction will revert.
solidity
Show full code block
// From MusicalChairsGameV2.sol
function executeEmergencyWithdrawal() external virtual onlyOwner nonReentrant {
if (emergencyWithdrawalProposalTimestamp == 0) revert NoEmergencyWithdrawalProposed();
if (block.timestamp < emergencyWithdrawalProposalTimestamp + DEFAULT_TIMELOCK_DELAY) revert TimelockNotPassed();
uint256 amountToWithdraw = proposedEmergencyWithdrawalAmount;
// ... reset state variables ...
(bool sent, ) = owner().call{value: amountToWithdraw}("");
if (!sent) revert ETHEmergencyWithdrawalFailed();
emit EmergencyWithdrawalExecuted(owner(), amountToWithdraw);
}
This is a night-and-day difference. The power to act in an emergency is retained, but the ability to abuse that power is eliminated. The community has a full week to see the proposal on-chain, discuss it, and, if necessary, exit the system.
Conclusion: Our Commitment to Building in Public
Our journey from an instant emergencyWithdrawETH function in V1 to the two-step, time-locked process in V2 is more than just a technical upgrade; it's a statement of our core philosophy. We believe that true decentralization means designing systems that are safe from their creators, not just for them. By placing all critical functions behind a mandatory, on-chain delay, we give the ultimate power back to the community: the power of observation and the time to react.
This commitment to transparency isn't just theoretical. Because every critical proposal—from ownership changes to contract upgrades—is subject to a 7-day timelock, these actions are public and verifiable on the blockchain long before they can be executed.
To make this monitoring accessible to everyone, we've created a public Dune dashboard that tracks these on-chain events. While we've never had to propose an emergency withdrawal, you can see this timelock mechanism in action right now with our pending upgrade to V4. This dashboard shows exactly what was proposed and how much time is left on the clock before the action can be executed. It's a real-time example of our promise in action.
We invite you to be an active participant in our security. Dive into our code, ask us the tough questions in our community channels, and monitor our on-chain activity. Your scrutiny makes us stronger.
- Review our Smart Contracts on GitHub: github.com/crow-004/musical-chairs-contracts
- Interact with the live contract on Arbiscan: arbiscan.io/address/0xEDA164585a5FF8c53c48907bD102A1B593bd17eF
- Learn how to report a vulnerability: View our SECURITY.md Thank you for helping keep Musical Chairs secure!

Top comments (0)