Solidity 简单合约实践

使用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 线下计算部分后续会更新。*****

相关推荐
FreeBuf_7 小时前
朝鲜APT组织使用Nim语言恶意软件对macOS发起隐秘Web3与加密货币攻击
macos·web3·策略模式
dingzd9515 小时前
结合指纹防护技术,释放Web3去中心化的潜力
web3·去中心化·区块链·facebook·tiktok·instagram·clonbrowser
OEC小胖胖15 小时前
去中心化身份:2025年Web3身份验证系统开发实践
前端·web3·去中心化·区块链
CertiK16 小时前
IBW 2025: CertiK首席商务官出席,探讨AI与Web3融合带来的安全挑战
人工智能·安全·web3
长安链开源社区10 天前
长安链浏览器「数据看板」 高效洞察数据状态与趋势
web3·区块链·共识算法
小碗细面10 天前
Web3学习记录之- Solidity安装使用Remix IDE
web3
穗余11 天前
WEB3开启 Hardhat 自动验证有什么意义
web3
代码羊羊11 天前
Solidity内部合约创建全解析:解锁Web3开发新姿势
web3·区块链
穗余11 天前
WEB3测试网的apikey如何获取?以及验证合约的方法
web3
躺在家里不干活11 天前
每日定投40刀BTC(19)20250609 - 20250622
web3·区块链