455 lines
14 KiB
Markdown
455 lines
14 KiB
Markdown
---
|
|
name: defi-protocol-templates
|
|
description: Implement DeFi protocols with production-ready templates for staking, AMMs, governance, and lending systems. Use when building decentralized finance applications or smart contract protocols.
|
|
---
|
|
|
|
# DeFi Protocol Templates
|
|
|
|
Production-ready templates for common DeFi protocols including staking, AMMs, governance, lending, and flash loans.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Building staking platforms with reward distribution
|
|
- Implementing AMM (Automated Market Maker) protocols
|
|
- Creating governance token systems
|
|
- Developing lending/borrowing protocols
|
|
- Integrating flash loan functionality
|
|
- Launching yield farming platforms
|
|
|
|
## Staking Contract
|
|
|
|
```solidity
|
|
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
contract StakingRewards is ReentrancyGuard, Ownable {
|
|
IERC20 public stakingToken;
|
|
IERC20 public rewardsToken;
|
|
|
|
uint256 public rewardRate = 100; // Rewards per second
|
|
uint256 public lastUpdateTime;
|
|
uint256 public rewardPerTokenStored;
|
|
|
|
mapping(address => uint256) public userRewardPerTokenPaid;
|
|
mapping(address => uint256) public rewards;
|
|
mapping(address => uint256) public balances;
|
|
|
|
uint256 private _totalSupply;
|
|
|
|
event Staked(address indexed user, uint256 amount);
|
|
event Withdrawn(address indexed user, uint256 amount);
|
|
event RewardPaid(address indexed user, uint256 reward);
|
|
|
|
constructor(address _stakingToken, address _rewardsToken) {
|
|
stakingToken = IERC20(_stakingToken);
|
|
rewardsToken = IERC20(_rewardsToken);
|
|
}
|
|
|
|
modifier updateReward(address account) {
|
|
rewardPerTokenStored = rewardPerToken();
|
|
lastUpdateTime = block.timestamp;
|
|
|
|
if (account != address(0)) {
|
|
rewards[account] = earned(account);
|
|
userRewardPerTokenPaid[account] = rewardPerTokenStored;
|
|
}
|
|
_;
|
|
}
|
|
|
|
function rewardPerToken() public view returns (uint256) {
|
|
if (_totalSupply == 0) {
|
|
return rewardPerTokenStored;
|
|
}
|
|
return rewardPerTokenStored +
|
|
((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / _totalSupply;
|
|
}
|
|
|
|
function earned(address account) public view returns (uint256) {
|
|
return (balances[account] *
|
|
(rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18 +
|
|
rewards[account];
|
|
}
|
|
|
|
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
|
|
require(amount > 0, "Cannot stake 0");
|
|
_totalSupply += amount;
|
|
balances[msg.sender] += amount;
|
|
stakingToken.transferFrom(msg.sender, address(this), amount);
|
|
emit Staked(msg.sender, amount);
|
|
}
|
|
|
|
function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
|
|
require(amount > 0, "Cannot withdraw 0");
|
|
_totalSupply -= amount;
|
|
balances[msg.sender] -= amount;
|
|
stakingToken.transfer(msg.sender, amount);
|
|
emit Withdrawn(msg.sender, amount);
|
|
}
|
|
|
|
function getReward() public nonReentrant updateReward(msg.sender) {
|
|
uint256 reward = rewards[msg.sender];
|
|
if (reward > 0) {
|
|
rewards[msg.sender] = 0;
|
|
rewardsToken.transfer(msg.sender, reward);
|
|
emit RewardPaid(msg.sender, reward);
|
|
}
|
|
}
|
|
|
|
function exit() external {
|
|
withdraw(balances[msg.sender]);
|
|
getReward();
|
|
}
|
|
}
|
|
```
|
|
|
|
## AMM (Automated Market Maker)
|
|
|
|
```solidity
|
|
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
contract SimpleAMM {
|
|
IERC20 public token0;
|
|
IERC20 public token1;
|
|
|
|
uint256 public reserve0;
|
|
uint256 public reserve1;
|
|
|
|
uint256 public totalSupply;
|
|
mapping(address => uint256) public balanceOf;
|
|
|
|
event Mint(address indexed to, uint256 amount);
|
|
event Burn(address indexed from, uint256 amount);
|
|
event Swap(address indexed trader, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out);
|
|
|
|
constructor(address _token0, address _token1) {
|
|
token0 = IERC20(_token0);
|
|
token1 = IERC20(_token1);
|
|
}
|
|
|
|
function addLiquidity(uint256 amount0, uint256 amount1) external returns (uint256 shares) {
|
|
token0.transferFrom(msg.sender, address(this), amount0);
|
|
token1.transferFrom(msg.sender, address(this), amount1);
|
|
|
|
if (totalSupply == 0) {
|
|
shares = sqrt(amount0 * amount1);
|
|
} else {
|
|
shares = min(
|
|
(amount0 * totalSupply) / reserve0,
|
|
(amount1 * totalSupply) / reserve1
|
|
);
|
|
}
|
|
|
|
require(shares > 0, "Shares = 0");
|
|
_mint(msg.sender, shares);
|
|
_update(
|
|
token0.balanceOf(address(this)),
|
|
token1.balanceOf(address(this))
|
|
);
|
|
|
|
emit Mint(msg.sender, shares);
|
|
}
|
|
|
|
function removeLiquidity(uint256 shares) external returns (uint256 amount0, uint256 amount1) {
|
|
uint256 bal0 = token0.balanceOf(address(this));
|
|
uint256 bal1 = token1.balanceOf(address(this));
|
|
|
|
amount0 = (shares * bal0) / totalSupply;
|
|
amount1 = (shares * bal1) / totalSupply;
|
|
|
|
require(amount0 > 0 && amount1 > 0, "Amount0 or amount1 = 0");
|
|
|
|
_burn(msg.sender, shares);
|
|
_update(bal0 - amount0, bal1 - amount1);
|
|
|
|
token0.transfer(msg.sender, amount0);
|
|
token1.transfer(msg.sender, amount1);
|
|
|
|
emit Burn(msg.sender, shares);
|
|
}
|
|
|
|
function swap(address tokenIn, uint256 amountIn) external returns (uint256 amountOut) {
|
|
require(tokenIn == address(token0) || tokenIn == address(token1), "Invalid token");
|
|
|
|
bool isToken0 = tokenIn == address(token0);
|
|
(IERC20 tokenIn_, IERC20 tokenOut, uint256 resIn, uint256 resOut) = isToken0
|
|
? (token0, token1, reserve0, reserve1)
|
|
: (token1, token0, reserve1, reserve0);
|
|
|
|
tokenIn_.transferFrom(msg.sender, address(this), amountIn);
|
|
|
|
// 0.3% fee
|
|
uint256 amountInWithFee = (amountIn * 997) / 1000;
|
|
amountOut = (resOut * amountInWithFee) / (resIn + amountInWithFee);
|
|
|
|
tokenOut.transfer(msg.sender, amountOut);
|
|
|
|
_update(
|
|
token0.balanceOf(address(this)),
|
|
token1.balanceOf(address(this))
|
|
);
|
|
|
|
emit Swap(msg.sender, isToken0 ? amountIn : 0, isToken0 ? 0 : amountIn, isToken0 ? 0 : amountOut, isToken0 ? amountOut : 0);
|
|
}
|
|
|
|
function _mint(address to, uint256 amount) private {
|
|
balanceOf[to] += amount;
|
|
totalSupply += amount;
|
|
}
|
|
|
|
function _burn(address from, uint256 amount) private {
|
|
balanceOf[from] -= amount;
|
|
totalSupply -= amount;
|
|
}
|
|
|
|
function _update(uint256 res0, uint256 res1) private {
|
|
reserve0 = res0;
|
|
reserve1 = res1;
|
|
}
|
|
|
|
function sqrt(uint256 y) private pure returns (uint256 z) {
|
|
if (y > 3) {
|
|
z = y;
|
|
uint256 x = y / 2 + 1;
|
|
while (x < z) {
|
|
z = x;
|
|
x = (y / x + x) / 2;
|
|
}
|
|
} else if (y != 0) {
|
|
z = 1;
|
|
}
|
|
}
|
|
|
|
function min(uint256 x, uint256 y) private pure returns (uint256) {
|
|
return x <= y ? x : y;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Governance Token
|
|
|
|
```solidity
|
|
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
contract GovernanceToken is ERC20Votes, Ownable {
|
|
constructor() ERC20("Governance Token", "GOV") ERC20Permit("Governance Token") {
|
|
_mint(msg.sender, 1000000 * 10**decimals());
|
|
}
|
|
|
|
function _afterTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 amount
|
|
) internal override(ERC20Votes) {
|
|
super._afterTokenTransfer(from, to, amount);
|
|
}
|
|
|
|
function _mint(address to, uint256 amount) internal override(ERC20Votes) {
|
|
super._mint(to, amount);
|
|
}
|
|
|
|
function _burn(address account, uint256 amount) internal override(ERC20Votes) {
|
|
super._burn(account, amount);
|
|
}
|
|
}
|
|
|
|
contract Governor is Ownable {
|
|
GovernanceToken public governanceToken;
|
|
|
|
struct Proposal {
|
|
uint256 id;
|
|
address proposer;
|
|
string description;
|
|
uint256 forVotes;
|
|
uint256 againstVotes;
|
|
uint256 startBlock;
|
|
uint256 endBlock;
|
|
bool executed;
|
|
mapping(address => bool) hasVoted;
|
|
}
|
|
|
|
uint256 public proposalCount;
|
|
mapping(uint256 => Proposal) public proposals;
|
|
|
|
uint256 public votingPeriod = 17280; // ~3 days in blocks
|
|
uint256 public proposalThreshold = 100000 * 10**18;
|
|
|
|
event ProposalCreated(uint256 indexed proposalId, address proposer, string description);
|
|
event VoteCast(address indexed voter, uint256 indexed proposalId, bool support, uint256 weight);
|
|
event ProposalExecuted(uint256 indexed proposalId);
|
|
|
|
constructor(address _governanceToken) {
|
|
governanceToken = GovernanceToken(_governanceToken);
|
|
}
|
|
|
|
function propose(string memory description) external returns (uint256) {
|
|
require(
|
|
governanceToken.getPastVotes(msg.sender, block.number - 1) >= proposalThreshold,
|
|
"Proposer votes below threshold"
|
|
);
|
|
|
|
proposalCount++;
|
|
Proposal storage newProposal = proposals[proposalCount];
|
|
newProposal.id = proposalCount;
|
|
newProposal.proposer = msg.sender;
|
|
newProposal.description = description;
|
|
newProposal.startBlock = block.number;
|
|
newProposal.endBlock = block.number + votingPeriod;
|
|
|
|
emit ProposalCreated(proposalCount, msg.sender, description);
|
|
return proposalCount;
|
|
}
|
|
|
|
function vote(uint256 proposalId, bool support) external {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(block.number >= proposal.startBlock, "Voting not started");
|
|
require(block.number <= proposal.endBlock, "Voting ended");
|
|
require(!proposal.hasVoted[msg.sender], "Already voted");
|
|
|
|
uint256 weight = governanceToken.getPastVotes(msg.sender, proposal.startBlock);
|
|
require(weight > 0, "No voting power");
|
|
|
|
proposal.hasVoted[msg.sender] = true;
|
|
|
|
if (support) {
|
|
proposal.forVotes += weight;
|
|
} else {
|
|
proposal.againstVotes += weight;
|
|
}
|
|
|
|
emit VoteCast(msg.sender, proposalId, support, weight);
|
|
}
|
|
|
|
function execute(uint256 proposalId) external {
|
|
Proposal storage proposal = proposals[proposalId];
|
|
require(block.number > proposal.endBlock, "Voting not ended");
|
|
require(!proposal.executed, "Already executed");
|
|
require(proposal.forVotes > proposal.againstVotes, "Proposal failed");
|
|
|
|
proposal.executed = true;
|
|
|
|
// Execute proposal logic here
|
|
|
|
emit ProposalExecuted(proposalId);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Flash Loan
|
|
|
|
```solidity
|
|
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
interface IFlashLoanReceiver {
|
|
function executeOperation(
|
|
address asset,
|
|
uint256 amount,
|
|
uint256 fee,
|
|
bytes calldata params
|
|
) external returns (bool);
|
|
}
|
|
|
|
contract FlashLoanProvider {
|
|
IERC20 public token;
|
|
uint256 public feePercentage = 9; // 0.09% fee
|
|
|
|
event FlashLoan(address indexed borrower, uint256 amount, uint256 fee);
|
|
|
|
constructor(address _token) {
|
|
token = IERC20(_token);
|
|
}
|
|
|
|
function flashLoan(
|
|
address receiver,
|
|
uint256 amount,
|
|
bytes calldata params
|
|
) external {
|
|
uint256 balanceBefore = token.balanceOf(address(this));
|
|
require(balanceBefore >= amount, "Insufficient liquidity");
|
|
|
|
uint256 fee = (amount * feePercentage) / 10000;
|
|
|
|
// Send tokens to receiver
|
|
token.transfer(receiver, amount);
|
|
|
|
// Execute callback
|
|
require(
|
|
IFlashLoanReceiver(receiver).executeOperation(
|
|
address(token),
|
|
amount,
|
|
fee,
|
|
params
|
|
),
|
|
"Flash loan failed"
|
|
);
|
|
|
|
// Verify repayment
|
|
uint256 balanceAfter = token.balanceOf(address(this));
|
|
require(balanceAfter >= balanceBefore + fee, "Flash loan not repaid");
|
|
|
|
emit FlashLoan(receiver, amount, fee);
|
|
}
|
|
}
|
|
|
|
// Example flash loan receiver
|
|
contract FlashLoanReceiver is IFlashLoanReceiver {
|
|
function executeOperation(
|
|
address asset,
|
|
uint256 amount,
|
|
uint256 fee,
|
|
bytes calldata params
|
|
) external override returns (bool) {
|
|
// Decode params and execute arbitrage, liquidation, etc.
|
|
// ...
|
|
|
|
// Approve repayment
|
|
IERC20(asset).approve(msg.sender, amount + fee);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Resources
|
|
|
|
- **references/staking.md**: Staking mechanics and reward distribution
|
|
- **references/liquidity-pools.md**: AMM mathematics and pricing
|
|
- **references/governance-tokens.md**: Governance and voting systems
|
|
- **references/lending-protocols.md**: Lending/borrowing implementation
|
|
- **references/flash-loans.md**: Flash loan security and use cases
|
|
- **assets/staking-contract.sol**: Production staking template
|
|
- **assets/amm-contract.sol**: Full AMM implementation
|
|
- **assets/governance-token.sol**: Governance system
|
|
- **assets/lending-protocol.sol**: Lending platform template
|
|
|
|
## Best Practices
|
|
|
|
1. **Use Established Libraries**: OpenZeppelin, Solmate
|
|
2. **Test Thoroughly**: Unit tests, integration tests, fuzzing
|
|
3. **Audit Before Launch**: Professional security audits
|
|
4. **Start Simple**: MVP first, add features incrementally
|
|
5. **Monitor**: Track contract health and user activity
|
|
6. **Upgradability**: Consider proxy patterns for upgrades
|
|
7. **Emergency Controls**: Pause mechanisms for critical issues
|
|
|
|
## Common DeFi Patterns
|
|
|
|
- **Time-Weighted Average Price (TWAP)**: Price oracle resistance
|
|
- **Liquidity Mining**: Incentivize liquidity provision
|
|
- **Vesting**: Lock tokens with gradual release
|
|
- **Multisig**: Require multiple signatures for critical operations
|
|
- **Timelocks**: Delay execution of governance decisions
|