使用solidity实现一个简单的合约
BTK.sol
solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
// • 向BTK合约转入token
// • 在BTK合约记录每个地址tokentoken
// • 编写 BTK合约 withdraw(), 实现只有管理员提取出所有的 token
// •用数组记录tokentoken前 3 名
contract BTK {
mapping(address => uint256) public balances;
struct TopUser {
address user;
uint256 amount;
}
TopUser[] public top3;
address public owner;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event Top3Updated(address[] indexed users, uint256[] indexed amounts);
constructor() {
owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
updateTop3(msg.sender);
}
function updateTop3(address user) internal {
// 检查是否已在 top3
bool found = false;
for (uint i = 0; i < top3.length; i++) {
if (top3[i].user == user) {
top3[i].amount = balances[user];
found = true;
break;
}
}
if (!found) {
if (top3.length < 3) {
top3.push(TopUser(user, balances[user]));
} else {
// 找到最小的替换
uint minIdx = 0;
for (uint i = 1; i < 3; i++) {
if (top3[i].amount < top3[minIdx].amount) {
minIdx = i;
}
}
if (balances[user] > top3[minIdx].amount) {
top3[minIdx] = TopUser(user, balances[user]);
}
}
}
// 排序,保证降序
for (uint i = 0; i < top3.length; i++) {
for (uint j = i + 1; j < top3.length; j++) {
if (top3[j].amount > top3[i].amount) {
TopUser memory temp = top3[i];
top3[i] = top3[j];
top3[j] = temp;
}
}
}
// emit 事件
address[] memory users = new address[](top3.length);
uint256[] memory amounts = new uint256[](top3.length);
for (uint i = 0; i < top3.length; i++) {
users[i] = top3[i].user;
amounts[i] = top3[i].amount;
}
emit Top3Updated(users, amounts);
}
function withdraw() public onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "Withdraw failed");
emit Withdraw(msg.sender, address(this).balance);
}
function getTop3()
public
view
returns (address[] memory, uint256[] memory)
{
address[] memory users = new address[](top3.length);
uint256[] memory amounts = new uint256[](top3.length);
for (uint i = 0; i < top3.length; i++) {
users[i] = top3[i].user;
amounts[i] = top3[i].amount;
}
return (users, amounts);
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
}
BTK.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {Test, console} from "../lib/forge-std/src/Test.sol";
import {BTK} from "../src/BTK.sol";
contract BTKTest is Test {
BTK public BTK;
function setUp() public {
BTK = new BTK();
}
address user1 = address(0x1);
address user2 = address(0x2);
address user3 = address(0x3);
address user4 = address(0x4);
function testDepositAndBalance() public {
vm.deal(user1, 10 ether);
vm.prank(user1);
BTK.deposit{value: 1 ether}();
assertEq(BTK.balances(user1), 1 ether);
vm.prank(user1);
BTK.deposit{value: 2 ether}();
assertEq(BTK.balances(user1), 3 ether);
}
function testTop3Ranking() public {
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
vm.deal(user3, 10 ether);
vm.deal(user4, 10 ether);
vm.prank(user1);
BTK.deposit{value: 1 ether}();
vm.prank(user2);
BTK.deposit{value: 2 ether}();
vm.prank(user3);
BTK.deposit{value: 3 ether}();
// user4 存入 4 ether,应该把 user1 挤出 top3
vm.prank(user4);
BTK.deposit{value: 4 ether}();
(address[] memory users, uint256[] memory amounts) = BTK.getTop3();
assertEq(users.length, 3);
assertEq(amounts.length, 3);
// top3 应该是 user4, user3, user2
assertEq(users[0], user4);
assertEq(amounts[0], 4 ether);
assertEq(users[1], user3);
assertEq(amounts[1], 3 ether);
assertEq(users[2], user2);
assertEq(amounts[2], 2 ether);
}
function testOnlyOwnerCanWithdraw() public {
// 非 owner 提现应 revert
vm.deal(user1, 10 ether);
vm.prank(user1);
vm.expectRevert("Only owner can call this function");
BTK.withdraw();
// owner 提现
vm.deal(address(this), 10 ether); // 确保 owner 有钱
BTK.deposit{value: 5 ether}();
uint256 before = address(this).balance;
BTK.withdraw();
uint256 afterBalance = address(this).balance;
assertEq(afterBalance, before + 5 ether);
}
function testFuzzDeposit(address user, uint256 amount) public {
// 限制 amount 范围,避免无效或溢出
amount = bound(amount, 1 wei, 100 ether);
vm.deal(user, amount);
vm.prank(user);
BTK.deposit{value: amount}();
assertEq(BTK.balances(user), amount);
}
receive() external payable {}
}
关键数据结构
mapping(address => uint256) public balances;
记录每个地址的token总额。struct TopUser { address user; uint256 amount; }
token榜单用户结构体。TopUser[] public top3;
token前 3 名榜单。address public owner;
管理员地址。
deposit()
payable
,允许接收 ETH。- 更新
balances
,并调用updateTop3
维护榜单。 - 触发
Deposit
事件。
updateTop3(address user)
- 检查用户是否已在榜单,若在则更新token。
- 若不在榜单且榜单未满,则直接加入。
- 若榜单已满且当前用户token大于榜单最小值,则替换。
- 最后对榜单按token降序排序。
- 触发
Top3Updated
事件。
BTK anvil测试
部署anvil
部署anvil
forge create src/BTK.sol:BTK --broadcast
查询onlyer
sh
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "owner()(address)"
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
token
sh
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "deposit()" --value 1ether
blockHash 0x37cac3c5373d5c58da0b75dadc69786e9130846a1a3a84a93d16d85a09ad8339
blockNumber 2
contractAddress
cumulativeGasUsed 117478
effectiveGasPrice 884244560
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 117478
logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"],"data":"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000","blockHash":"0x37cac3c5373d5c58da0b75dadc69786e9130846a1a3a84a93d16d85a09ad8339","blockNumber":"0x2","blockTimestamp":"0x684104fa","transactionHash":"0x6607103647595b78dbf64aa225f7ca5a370b4febd68faeedf2151d66eb01e073","transactionIndex":"0x0","logIndex":"0x0","removed":false},{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0x0e3aaf1f8d49c5908d9350cc325f7f1ffc31b81448f1b91c7307f9865953c18b","0xd9c5115d8ca09413513b0348ccd4aa5d5d2b8183823763b527bfd81f40d86f2a","0xeb56b1d3ea8cb70aecb2523bb3f1061affbd3cc2b5d6307a2fafc6a1c1f4c868"],"data":"0x","blockHash":"0x37cac3c5373d5c58da0b75dadc69786e9130846a1a3a84a93d16d85a09ad8339","blockNumber":"0x2","blockTimestamp":"0x684104fa","transactionHash":"0x6607103647595b78dbf64aa225f7ca5a370b4febd68faeedf2151d66eb01e073","transactionIndex":"0x0","logIndex":"0x1","removed":false}]
logsBloom 0x0000000000000000000000000002000000000000000000000000000000020000000000000000000000000000000004000000000000000000000040000000000000000000000000000000000000000200000000000000000000000000800000000000004000000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000100000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000004000000020000000000000000000000000a000000000000000000020000000000000000000000000000000000000000000000000440000000000000000
root
status 1 (success)
transactionHash 0x6607103647595b78dbf64aa225f7ca5a370b4febd68faeedf2151d66eb01e073
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
查询
sh
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "balances(address)(uint256)" 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1000000000000000000 [1e18]
查询 top3
sh
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "getTop3()(address[],uint256[])"
[0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266]
[1000000000000000000 [1e18]]
管理员提取
sh
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "withdraw()"
blockHash 0xa8bdc886a533b4465239f9974f2c4f94da0b56b8b1d406579577b386e0618cfd
blockNumber 3
contractAddress
cumulativeGasUsed 30280
effectiveGasPrice 774579651
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 30280
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0x1a9be912a87fcae32192a251eec28ebe66ce4a9e5d0028a0161d0704525ebfdf
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
事件日志查询
sh
cast logs --from-block 0 --to-block latest --rpc-url http://127.0.0.1:8545
- address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
blockHash: 0x37cac3c5373d5c58da0b75dadc69786e9130846a1a3a84a93d16d85a09ad8339
blockNumber: 2
data: 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000
logIndex: 0
removed: false
topics: [
0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
]
transactionHash: 0x6607103647595b78dbf64aa225f7ca5a370b4febd68faeedf2151d66eb01e073
transactionIndex: 0
- address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
blockHash: 0x37cac3c5373d5c58da0b75dadc69786e9130846a1a3a84a93d16d85a09ad8339
blockNumber: 2
data: 0x
logIndex: 1
removed: false
topics: [
0x0e3aaf1f8d49c5908d9350cc325f7f1ffc31b81448f1b91c7307f9865953c18b
0xd9c5115d8ca09413513b0348ccd4aa5d5d2b8183823763b527bfd81f40d86f2a
0xeb56b1d3ea8cb70aecb2523bb3f1061affbd3cc2b5d6307a2fafc6a1c1f4c868
]
transactionHash: 0x6607103647595b78dbf64aa225f7ca5a370b4febd68faeedf2151d66eb01e073
transactionIndex: 0
BTK sepolia
部署验证开源
sh
forge create src/BTK.sol:BTK --broadcast --verify -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Deployer: 0xeeb0F1Dc21d47B81545c923523eE345002A2E5C0
Deployed to: 0x36A652755c0174f94B0de4Dc50bF4488319F0637
Transaction hash: 0x5cf86df4d423b224efdfd7579844b3decab385beeb1b3bca74acb7dbfb953437
Starting contract verification...
Waiting for sourcify to detect contract deployment...
Start verifying contract `0x36A652755c0174f94B0de4Dc50bF4488319F0637` deployed on sepolia
Compiler version: 0.8.30
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (4 tries remaining)
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (3 tries remaining)
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (2 tries remaining)
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (1 tries remaining)
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (0 tries remaining)
Submitting verification for [src/BTK.sol:BTK] 0x36A652755c0174f94B0de4Dc50bF4488319F0637.
Submitted contract for verification:
Response: `OK`
GUID: `56apmvev7e4k4sahs5gwr2spjnyqbqyvfut3hjsuqc7vdgqh3k`
URL: https://sepolia.etherscan.io/address/0x36a652755c0174f94b0de4dc50bf4488319f0637
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Warning: Verification is still pending...; waiting 15 seconds before trying again (7 tries remaining)
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
格式化abi
sh
cat out/BTK.sol/BTK.json | jq '.abi' > BTK.abi.json
优化一:不需要getTop3 怎加了额外的算力 使用top3(0) 使用top3(1) 使用top3(2)
sh
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "top3(uint256)(address,uint256)" 0
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
1000000000000000000 [1e18]
优化二:使用call 存提无gaslimit限制 receive()可接收无data call
sh
function withdraw() public onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "Withdraw failed");
}
receive() external payable {
deposit();
}
优化三:updateTop3的计算逻辑可放于线下 线上只存储top3
优化四:是用 The Graph 收集事件日志并计算榜单
优化后的完整code
sh
contract BTK {
mapping(address => uint256) public balances;
struct TopUser {
address user;
uint256 amount;
}
TopUser[3] public top3;
address public owner;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event Top3Updated(address[] users, uint256[] amounts);
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 线下计算
function setTop3(address[] calldata users, uint256[] calldata amounts) external onlyOwner {
require(users.length == amounts.length, "users/amounts length mismatch");
require(users.length <= 3, "At most 3 users");
// 清空原榜单
for (uint i = 0; i < 3; i++) {
if (i < users.length) {
top3[i] = TopUser(users[i], amounts[i]);
} else {
top3[i] = TopUser(address(0), 0); // 空位补零
}
}
// 构造完整榜单用于事件
address[] memory fullUsers = new address[](3);
uint256[] memory fullAmounts = new uint256[](3);
for (uint i = 0; i < 3; i++) {
fullUsers[i] = top3[i].user;
fullAmounts[i] = top3[i].amount;
}
emit Top3Updated(fullUsers, fullAmounts);
}
function withdraw() public onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "Withdraw failed");
emit Withdraw(msg.sender, address(this).balance);
}
//支持无data call
receive() external payable {
deposit();
}
}
*****感谢您的查阅!The Graph 线下计算部分后续会更新。*****