As part of Week 4 of my EtherAuthority Web3 Internship, I completed a full-stack DeFi Staking Platform that allows users to stake ERC-20 STK tokens and earn RWD reward tokens based on the time their tokens remain staked.
The project combines Solidity smart contracts, Foundry-based testing and deployment, OpenZeppelin contracts, a Next.js frontend, TypeScript, ethers.js and MetaMask wallet integration.
The final smart contracts are deployed on the Ethereum Sepolia Testnet, and the frontend is deployed using Vercel.
The objective of this project was to build a working staking dApp from the smart-contract layer to the user interface.
The platform implements:
The repository describes the project as a full-stack DeFi staking platform built with Solidity, Foundry, OpenZeppelin, Next.js, TypeScript, ethers.js and MetaMask.
^0.8.1816.3.019.2.86.17.0The frontend dependency configuration confirms Next.js, React, React DOM and ethers.js as the main runtime dependencies.
The platform follows a straightforward Web3 dApp architecture:
User
│
▼
MetaMask Wallet
│
▼
Next.js / React Frontend
│
ethers.js
│
┌──────────┴──────────┐
│ │
▼ ▼
StakeToken (STK) Staking Contract
│ │
│ ├── stake()
│ ├── Withdraw()
│ ├── claimRewards()
│ ├── setAPR()
│ ├── pause()
│ └── unpause()
│
▼
RewardToken (RWD)
Ethereum Sepolia
The frontend creates ethers.js contract instances using the configured STK, RWD and staking contract addresses. It reads the connected wallet's balances and staking information directly from the blockchain.
The project contains three main Solidity contracts:
StakeToken.solRewardToken.solStaking.solA Foundry deployment script deploys the three contracts in sequence.
StakeToken is an OpenZeppelin ERC-20 token.
It uses:
ERC20("Stake Token", "STK")
During deployment, the constructor mints:
100,000 STK
to the deploying address.
This contract therefore provides the token users approve and stake in the staking contract.
The standard ERC-20 functionality inherited from OpenZeppelin provides operations such as:
balanceOftransferapprovetransferFromThe reward token is another OpenZeppelin ERC-20 token:
ERC20("Reward Token", "RWD")
The constructor mints:
1,000,000 RWD
to the deploying address.
The staking contract later transfers RWD rewards to users when they claim their accumulated rewards.
The Staking contract is the core of the application.
It inherits from:
Ownable
Pausable
ReentrancyGuard
and uses OpenZeppelin's SafeERC20.
The contract maintains:
mapping(address => uint256) public stakedBalance;
mapping(address => uint256) public pendingRewards;
mapping(address => uint256) public lastUpdated;
It also maintains the total public stake and an APR value that starts at:
10%
The contract defines custom errors including:
ZeroAmountInsufficientStakeNoRewardsInvalidAPRThe frontend first requires the user to approve the staking contract to spend the requested amount of STK.
The frontend calls:
STK.approve(StakingContract, amount)
After the approval transaction is confirmed, the user can click Stake.
The staking contract then calls:
stakeToken.safeTransferFrom(
msg.sender,
address(this),
amount
);
and increases the user's recorded stake:
stakedBalance[msg.sender] += amount;
The global publicStake value is also increased.
The contract records the timestamp used for subsequent reward calculations.
Rewards are calculated when the user's staking state is updated.
The contract calculates elapsed time using:
block.timestamp - lastUpdated[user]
and calculates the reward using:
(stakedBalance[user] * apr * timeElapsed)
/ (100 * YEAR)
where:
YEAR = 365 * 24 * 60 * 60;
The default APR is 10%.
The accumulated amount is added to:
pendingRewards[user]
This means rewards are based on:
rather than being generated from a separate external price feed or oracle.
Users can withdraw previously staked STK using:
Withdraw(uint256 amount)
Before processing the withdrawal, the contract updates the user's rewards.
It then checks that the requested amount does not exceed the user's recorded stake.
After the check, the contract decreases:
stakedBalance[msg.sender]
and:
publicStake
before transferring the STK back to the user.
The function is protected by both nonReentrant and whenNotPaused.
Users can claim accumulated RWD using:
claimRewards()
The function first updates the user's pending rewards.
If there are no rewards, the contract reverts with:
NoRewards()
Otherwise, the pending reward amount is reset to zero and the RWD tokens are transferred to the user.
The contract emits a RewardClaimed event after a successful claim.
The contract owner can update the APR through:
setAPR(uint256 newAPR)
The function is protected with:
onlyOwner
and rejects an APR value of zero.
The contract emits:
APRupdated(oldAPR, newAPR)
after a successful update.
The staking contract also implements emergency-style pause functionality.
The owner can call:
pause()
and:
unpause()
The staking, withdrawal and reward-claiming functions use whenNotPaused.
This provides an administrative mechanism to temporarily stop those operations when the contract is paused.
The frontend is implemented using Next.js and React with TypeScript.
The application uses ethers.js to communicate with the deployed contracts.
The ConnectWallet component checks whether MetaMask is available through:
window.ethereum
It then creates an ethers.js BrowserProvider, requests the user's account and obtains a signer.
The connected wallet address is passed to the main application.
The application displays the connected wallet address and uses it when querying blockchain data.
After connecting the wallet, the frontend reads:
The STK balance is obtained using:
token.balanceOf(address)
The staking contract is queried for:
staking.stakedBalance(address)
staking.pendingRewards(address)
staking.apr()
The values are formatted from their on-chain units using ethers.js.
The frontend also refreshes blockchain data periodically after the wallet is connected.
The implemented user flow is:
Connect MetaMask
↓
Enter STK amount
↓
Approve STK
↓
Confirm MetaMask transaction
↓
Stake
↓
Confirm MetaMask transaction
↓
Staking contract receives STK
↓
Dashboard refreshes
↓
Rewards accumulate over time
↓
Withdraw or Claim Rewards
The frontend has separate buttons for:
The transaction hash is displayed after transactions are submitted, with a direct link to Sepolia Etherscan.
I used Foundry to test the staking contract behavior.
The test setup deploys fresh instances of:
before running the test cases.
The test suite covers:
It verifies that the staking contract stores the correct STK and RWD token addresses.
A test transfers STK to a test user, approves the staking contract and stakes 100 STK.
It verifies that the user's staked balance and total public stake are updated correctly.
The test stakes 100 STK and withdraws 40 STK.
It verifies that:
Staked balance = 60 STK
Wallet balance = 40 STK
after the withdrawal.
The tests verify that:
The reward test funds the staking contract with RWD, stakes 100 STK, advances the blockchain time by 365 days using Foundry's vm.warp, and then claims the rewards.
The test verifies that rewards are received and pending rewards are reset.
The tests verify that the owner can change the APR while a non-owner cannot.
The tests verify that staking is blocked while the contract is paused and works again after the contract is unpaused.
The Foundry configuration uses Solidity compiler version 0.8.20.
The final deployment is on Ethereum Sepolia Testnet.
The deployment script creates the contracts in this order:
StakeToken
↓
RewardToken
↓
Staking
The staking contract receives the deployed STK and RWD addresses as constructor parameters.
The deployment records in the repository identify chain ID:
11155111
with hexadecimal chain ID:
0xaa36a7
The deployment transactions were successfully recorded on-chain.
| Contract | Address |
|---|---|
| STK — StakeToken | 0xaba6599a21cf3fa1bcf4038eb225f829bdb17cf5 |
| RWD — RewardToken | 0x36070c3ad6efa8045d87e97e627dcb0eced73279 |
| Staking | 0x1873eca046e15b910da2ea9ad50fce9fd695b0a7 |
These addresses are the same addresses configured in the frontend.
Transaction hash:
0xf5b5bd8ddd3433053180654d1737e595b90255a06b902bc5a8b7512acc2643b1
Contract:
0xaba6599a21cf3fa1bcf4038eb225f829bdb17cf5
Transaction hash:
0xb8dd980a8eb4d6dd3fe464e4c21f97845a7be26c0b07d8ea76416ef623251a00
Contract:
0x36070c3ad6efa8045d87e97e627dcb0eced73279
Transaction hash:
0x350b4bdba54a3fdc129e4237b5d0c8d6d9a91194825b92791a693f401437dfd8
Contract:
0x1873eca046e15b910da2ea9ad50fce9fd695b0a7
The deployment records show these transactions were created on the Sepolia chain and that the staking deployment received the STK and RWD addresses as constructor arguments.
One of the important implementation details was understanding that the staking contract cannot simply take STK from a user's wallet.
The user must first authorize the staking contract through:
approve()
and then call:
stake()
I implemented these as separate actions in the frontend and handled the transaction confirmations and errors through the UI.
During development, staking functionality required debugging between the frontend transaction flow and the staking contract.
The final repository contains a commit titled:
“Fix staking functionality and update UI”
which represents one of the final implementation updates.
Another important practical lesson came from testing the deployed application with different wallets.
The frontend reads the STK balance using:
balanceOf(connected wallet address)
Therefore, the displayed balance belongs to the currently connected wallet; it is not a universal balance provided to every visitor of the application.
This was an important lesson in understanding the difference between frontend state, wallet identity and on-chain token ownership.
The project uses an .env file for local environment configuration, and .env is included in the repository's .gitignore. I did not include or publish any private key, seed phrase or secret credential in this article.
The following can be added to the published article:
Show the connected MetaMask wallet and the wallet address displayed by the application.
Show the MetaMask approval transaction and the successful approval message.
Show the staking transaction and updated STK/staked balances.
Show pending RWD rewards after staking and the reward-claim transaction.
Show the Sepolia network and the deployed STK, RWD and Staking contract addresses.
A short walkthrough can demonstrate:
Connect Wallet
→ Check STK balance
→ Approve STK
→ Stake
→ Wait for reward accumulation
→ Withdraw
→ Claim Rewards
GitHub Repository
https://github.com/PranshuMishra2004/DeFi-Staking-Platform
Live Project
https://de-fi-staking-platform-opal.vercel.app/
The GitHub repository contains the Solidity contracts, Foundry tests, deployment records and frontend implementation.
The EtherAuthority training instructions require public evidence and GitHub/repository information for final submission, but the available instructions do not define a field or identifier called “Project Hash.”
The current GitHub repository's latest commit is:
294150d188216f1da530bccd39a439021a737ff6
Commit:
Fix staking functionality and update UI
This is the Git commit SHA, not a confirmed EtherAuthority “Project Hash.”
Therefore, I would only enter this value as the Project Hash in the internship workbook if EtherAuthority confirms that they mean the Git commit SHA. I would not invent a separate hash.
This project gave me practical experience across the complete Web3 development workflow:
Solidity
↓
ERC-20 Tokens
↓
Staking Contract
↓
Foundry Testing
↓
Sepolia Deployment
↓
ethers.js Integration
↓
MetaMask
↓
Next.js Frontend
↓
Vercel Deployment
The most valuable part of the project was not only writing the smart contracts, but understanding how the contracts, wallet, frontend and blockchain state interact as one complete application.
Completing this DeFi Staking Platform during Week 4 allowed me to bring together the concepts I learned throughout the EtherAuthority Web3 training program into one working dApp.
The project covers ERC-20 token creation, staking, withdrawals, time-based rewards, reward claiming, access control, pause functionality, Foundry testing, Ethereum Sepolia deployment and frontend wallet integration.
The final result is a live staking application with its smart contracts deployed on Ethereum Sepolia and a publicly accessible frontend.
This project also helped me understand an important principle of Web3 development: the frontend is only an interface. The actual balances, staking positions, rewards and transactions are determined by the smart contracts and the connected wallet's on-chain state.