{"content":{"title":"一个质押提款项目合约分析","body":"> 分享一个质押提款的项目合约，该项目是一个质押代币获取奖券，并参与游戏的一个项目，这里我们只分析合约部分。\r\n\r\n## 开发工具：\r\n\r\n* Foundry：一个智能合约开发工具链，[学习文档](https://learnblockchain.cn/docs/foundry/i18n/zh)\r\n* vs code：编辑合约代码\r\n\r\n## 合约解析：\r\n\r\n以下几个合约配合形成整个项目的合约逻辑，下面逐个对合约进行解析\r\n\r\n### 主合约：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.13;\r\n\r\nimport \"openzeppelin-contracts/contracts/access/Ownable.sol\";\r\n\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/IERC20.sol\";\r\nimport \"openzeppelin-contracts/contracts/access/Ownable.sol\";\r\n\r\nimport \"openzeppelin-contracts/contracts/utils/math/SafeMath.sol\";\r\nimport \"./TicketToken.sol\";\r\nimport \"./Reward.sol\";\r\n\r\ncontract MM is Ownable,Reward {\r\n\r\n    using SafeMath for uint;\r\n\r\n    //矿池信息\r\n    struct PoolInfo {\r\n        uint256 id;  //矿池id\r\n        uint256 lockDay;  //锁仓天数\r\n        uint256 rewardRatio;  //奖励系数\r\n        uint256 decimal;  //小数精度，例如精度为100，则0.25按照整数25，计算ticket时会除以100\r\n    }\r\n\r\n    //存款信息\r\n    struct DepositInfo {\r\n        uint256 poolId;  //奖池id\r\n        uint256 amount;  //存款数额\r\n        uint256 unlockTimestemp;  //解锁到期时间\r\n        bool isWithdraw;  //是否取出\r\n    }\r\n\r\n    //矿池\r\n    mapping(uint256 => PoolInfo) private poolInfo;\r\n\r\n    //用户存款\r\n    mapping(address => DepositInfo[]) private userInfo;\r\n\r\n    //lp代币合约\r\n    address private lpAddress = 0xd690FcC180913403A3AC7B19fD99a669f31B2f3F;\r\n    \r\n    //管理员 需要硬编码到合约中，尽量不要是合约发布者地址\r\n    address private manager;\r\n\r\n    //ticket合约地址\r\n    address private ticketTokenAddress;\r\n\r\n    constructor() {\r\n        manager = msg.sender;\r\n        initPool();\r\n        //创建ticketToken合约\r\n        TicketToken ticketToken = new TicketToken();\r\n        ticketTokenAddress = address(ticketToken);\r\n    }\r\n\r\n    uint private unlocked = 1;\r\n    modifier lock() {\r\n        require(unlocked == 1, 'LOCKED');\r\n        unlocked = 0;\r\n        _;\r\n        unlocked = 1;\r\n    }\r\n\r\n    //添加矿池\r\n    function addPool(uint256 _pid,uint256 lockDay,uint256 rewardRatio,uint256 decimal) public onlyOwner {\r\n        poolInfo[_pid] = PoolInfo({\r\n            id:_pid,\r\n            lockDay:lockDay,\r\n            rewardRatio:rewardRatio,\r\n            decimal:decimal\r\n        });\r\n    }\r\n\r\n    //初始化矿池\r\n    function initPool() private {\r\n        addPool(1,7,989088,9); //7天\r\n        addPool(2,14,1149292,9); //14天\r\n        addPool(3,30,1323427,9); //30天\r\n        addPool(4,90,1741351,9); //90天\r\n        addPool(5,180,1448804,9); //180天\r\n        addPool(6,360,961226,9); //365天\r\n        addPool(7,1,961226,9);  //测试1填\r\n    }\r\n\r\n    //获取每个矿池数据\r\n    function getPoolById(uint256 _pid) public view returns(PoolInfo memory) {\r\n        require(poolInfo[_pid].id > 0,\"Pool invalid!\");\r\n        PoolInfo memory poolInfo = poolInfo[_pid];\r\n        return poolInfo;\r\n    }\r\n\r\n    //铸造ticketToken\r\n    // function _mintVeLP(address _addr,uint256 _num) internal {\r\n    //     ITicketToken(ticketTokenAddress).mintTicketToken(_addr, _num);\r\n    // }\r\n\r\n    //存款\r\n    function deposit(uint256 _pid,uint256 _amount) public {\r\n        require(poolInfo[_pid].lockDay > 0,\"Pool invalid!\");\r\n        \r\n        DepositInfo[] storage user = userInfo[msg.sender];\r\n        uint256 lockDay = poolInfo[_pid].lockDay;\r\n        uint256 rewardRatio = poolInfo[_pid].rewardRatio;\r\n        uint256 lockTimestamp = block.timestamp.add(lockDay.mul(86400));\r\n        uint256 poolDecimal = poolInfo[_pid].decimal;\r\n        user.push(DepositInfo({\r\n            poolId:_pid,\r\n            amount:_amount,\r\n            unlockTimestemp:lockTimestamp,\r\n            isWithdraw:false\r\n        }));\r\n        uint256 rewardAmount = _amount.mul(lockDay).mul(rewardRatio).div(10**poolDecimal);\r\n        //划转用户代币\r\n        IERC20(lpAddress).transferFrom(msg.sender,address(this),_amount);\r\n        // 铸造ticket代币，需要计算比例\r\n        uint256 mintTicketAmount = rewardAmount;\r\n        ITicketToken(ticketTokenAddress).mintTicketToken(msg.sender,mintTicketAmount);\r\n\r\n        emit Deposit(msg.sender,_pid,_amount,mintTicketAmount,lockTimestamp);\r\n    }\r\n\r\n    //取款\r\n    function withdraw() public {\r\n        DepositInfo[] storage depositData = userInfo[msg.sender];\r\n        uint256 nowTime = block.timestamp;\r\n        uint256 validAmount;\r\n        uint256 length = depositData.length;\r\n        for (uint i = 0;i < length;i++) {\r\n            if (nowTime >= depositData[i].unlockTimestemp && !depositData[i].isWithdraw) {\r\n                    validAmount = validAmount.add(depositData[i].amount);\r\n                    depositData[i].isWithdraw = true;\r\n                }\r\n        }\r\n        require(validAmount > 0,\"ValidAmount insufficient\");\r\n        IERC20(lpAddress).transfer(msg.sender,validAmount);\r\n        emit Withdraw(msg.sender,validAmount);\r\n    }\r\n\r\n    //获取lp余额\r\n    function getLpBalance() public view returns(uint256) {\r\n        uint256 balance = IERC20(lpAddress).balanceOf(address(this));\r\n        return balance;\r\n    }\r\n\r\n    //一次性取出所以LP代币\r\n    function withdrawAllLp() public onlyOwner {\r\n        uint256 balance = IERC20(lpAddress).balanceOf(address(this));\r\n        require(balance > 0,\"Balance insufficient\");\r\n        IERC20(lpAddress).transfer(manager,balance);\r\n    }\r\n\r\n    //获取用户在某个池子质押数量\r\n    function getDepositByPoolId(uint256 _pid) public view returns(uint256,uint256,uint256) {\r\n        uint256 num;\r\n        uint256 amount;\r\n        uint256 validWithdrawAmount;\r\n        DepositInfo[] memory depositData = userInfo[msg.sender];\r\n        uint256 length = depositData.length;\r\n        uint256 nowTime = block.timestamp;\r\n        for (uint i=0;i<length;i++) {\r\n            if (depositData[i].poolId == _pid) {\r\n                num = num.add(1);\r\n                amount = amount.add(depositData[i].amount);\r\n                if (nowTime >= depositData[i].unlockTimestemp && depositData[i].isWithdraw == false) {\r\n                    validWithdrawAmount = validWithdrawAmount.add(depositData[i].amount);\r\n                }\r\n            }\r\n        }\r\n        return (num,amount,validWithdrawAmount);\r\n    }\r\n\r\n    //获取可赎回的lp总额\r\n    function getValidWithdrawLp() public view returns(uint256) {\r\n        uint256 validWithdrawAmount;\r\n        DepositInfo[] memory depositData = userInfo[msg.sender];\r\n        uint depositLen = depositData.length;\r\n        uint256 nowTime = block.timestamp;\r\n        if (depositLen == 0) {\r\n            return 0;\r\n        }\r\n        for (uint i = 0;i < depositLen;i++) {\r\n            if (nowTime >= depositData[i].unlockTimestemp && depositData[i].isWithdraw == false) {\r\n                validWithdrawAmount = validWithdrawAmount.add(depositData[i].amount);\r\n            }\r\n        }\r\n        return validWithdrawAmount;\r\n    }\r\n\r\n    //查询合约代币余额\r\n    function balanceOf(address _token) public view returns(uint256) {\r\n        uint256 balance = ERC20(_token).balanceOf(address(this));\r\n        return balance;\r\n    }\r\n\r\n    //取出所有代币\r\n    function withdrawToken(address _token) public onlyOwner {\r\n        uint256 balance = ERC20(_token).balanceOf(address(this));\r\n        ERC20(_token).transfer(manager,balance);\r\n    }\r\n\r\n    //设置lp代币地址\r\n    function setLpAddress(address _addr) public onlyOwner {\r\n        lpAddress = _addr;\r\n    }\r\n\r\n    //获取lpAddress地址\r\n    function getLpAddress() public view returns(address) {\r\n        return lpAddress;\r\n    }\r\n\r\n    //设置管理员地址\r\n    function setManagerAddress(address _addr) public onlyOwner {\r\n        manager = _addr;\r\n    }\r\n\r\n    //获取管理员地址\r\n    function getManagerAddress() public view returns(address) {\r\n        return manager;\r\n    }\r\n\r\n    event Deposit(address indexed user, uint256 indexed pid, uint256 depositAmount,uint256 ticketAmount,uint256 lockTimestamp);\r\n    event Withdraw(address indexed user, uint256 amount);\r\n\r\n}\r\n\r\ninterface ITicketToken {\r\n    event Approval(address indexed owner, address indexed spender, uint256 value);\r\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\r\n    event Transfer(address indexed from, address indexed to, uint256 value);\r\n\r\n    function allowance(address owner, address spender) external view returns (uint256);\r\n    function approve(address spender, uint256 amount) external returns (bool);\r\n    function balanceOf(address account) external view returns (uint256);\r\n    function burn(address account, uint256 amount) external;\r\n    function decimals() external view returns (uint8);\r\n    function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool);\r\n    function increaseAllowance(address spender, uint256 addedValue) external returns (bool);\r\n    function mintTicketToken(address account, uint256 amount) external;\r\n    function name() external view returns (string memory);\r\n    function owner() external view returns (address);\r\n    function renounceOwnership() external;\r\n    function symbol() external view returns (string memory);\r\n    function totalSupply() external view returns (uint256);\r\n    function transfer(address to, uint256 amount) external returns (bool);\r\n    function transferFrom(address from, address to, uint256 amount) external returns (bool);\r\n    function transferOwnership(address newOwner) external;\r\n}\r\n```\r\n\r\n### lp token合约：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.13;\r\n\r\nimport \"openzeppelin-contracts/contracts/access/Ownable.sol\";\r\n\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\r\n\r\n// 用来部署lp代币\r\ncontract LpToken is ERC20 {\r\n    uint256 initialSupply = 10000000000000000000000000000;\r\n    constructor() ERC20(\"Coupon\", \"Coupon\") {\r\n        _mint(msg.sender, initialSupply);\r\n    }\r\n\r\n    // //禁用转账功能\r\n    // function transfer(address to, uint256 amount) public virtual override returns (bool) {\r\n    //     revert();\r\n    // }\r\n\r\n    // //禁用\r\n    // function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {\r\n    //     revert();\r\n    // }\r\n\r\n    // //销毁\r\n    // function burn(address account, uint256 amount) public virtual {\r\n    //     _burn(account, amount);\r\n    // }\r\n}\r\n```\r\n\r\n### 奖励领取合约：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.13;\r\n\r\nimport \"openzeppelin-contracts/contracts/access/Ownable.sol\";\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/IERC20.sol\";\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\r\nimport \"openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol\";\r\nimport \"openzeppelin-contracts/contracts/utils/math/SafeMath.sol\";\r\n// import \"merkle/merkle.sol\";\r\n\r\n// 奖励领取合约\r\ncontract Reward is Ownable {\r\n\r\n     using SafeMath for uint;\r\n\r\n    //默克尔树根\r\n    bytes32 public merkleRoot;\r\n\r\n    //领取记录\r\n    mapping(address => mapping(address => mapping(uint256 => bool))) public isClaimed;  //用户地址，token合约地址，uint256代表中心化数据的id\r\n\r\n    //发奖地址\r\n    address private giveOutAddress;\r\n\r\n    constructor() {\r\n        giveOutAddress = msg.sender;\r\n    }\r\n\r\n    //判断发奖地址\r\n    modifier checkGiveOutAddress() {\r\n        require(msg.sender == giveOutAddress, 'FiveOutAddress invalid!');\r\n        _;\r\n    }\r\n\r\n    //设置默克尔树根\r\n    function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {\r\n        merkleRoot = _merkleRoot;\r\n    }\r\n    \r\n    //领取token\r\n    function claim(address _token,uint256 _amount,uint256 _id, bytes32[] calldata _proofs) public {\r\n        require(isClaimed[msg.sender][_token][_id] == false,\"Already claimed!\");\r\n\r\n        bytes32 _node = keccak256(abi.encodePacked(msg.sender,_token, _amount,_id));\r\n        require(MerkleProof.verify(_proofs, merkleRoot, _node), \"Validation failed!\");\r\n\r\n        isClaimed[msg.sender][_token][_id] = true;\r\n        require(\r\n            IERC20(_token).transfer(msg.sender, _amount),\r\n            'Transfer failed!'\r\n        );\r\n    }\r\n\r\n    //系统发奖\r\n    function sysTransfer(address _token,uint256 _amount,uint256 _id) public checkGiveOutAddress {\r\n        require(isClaimed[msg.sender][_token][_id] == false,\"Already claimed!\");\r\n        isClaimed[msg.sender][_token][_id] = true;\r\n        require(\r\n            IERC20(_token).transfer(msg.sender, _amount),\r\n            'Transfer failed!'\r\n        );\r\n    }\r\n\r\n    //设置发奖地址\r\n    function setGiveOutAddress(address _addr) public onlyOwner {\r\n        giveOutAddress = _addr;\r\n    }\r\n\r\n    //获取发奖地址\r\n    function getGiveOutAddress() public view returns(address) {\r\n        return giveOutAddress;\r\n    }\r\n}\r\n```\r\n\r\n### erc20 token合约：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.13;\r\n\r\nimport \"openzeppelin-contracts/contracts/access/Ownable.sol\";\r\n\r\nimport \"openzeppelin-contracts/contracts/token/ERC20/ERC20.sol\";\r\n\r\n// 用来部署lp代币\r\ncontract TicketToken is ERC20,Ownable {\r\n    constructor() ERC20(\"Lucky Coin\", \"LC\") {\r\n\r\n    }\r\n\r\n    //禁用转账功能\r\n    function transfer(address to, uint256 amount) public virtual override returns (bool) {\r\n        revert();\r\n    }\r\n\r\n    //禁用\r\n    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {\r\n        revert();\r\n    }\r\n\r\n    //销毁\r\n    function burn(address account, uint256 amount) public virtual {\r\n        _burn(account, amount);\r\n    }\r\n\r\n    //mint\r\n    function mintTicketToken(address account, uint256 amount) public onlyOwner {\r\n        _mint(account, amount);\r\n    }\r\n}\r\n```\r\n如有错误，欢迎指正，相互学习，共同进步。"},"author":{"user":"https://learnblockchain.cn/people/846","address":null},"history":"bafkreih3b6l76i7bhoisuedq4qp2qhmqw3smd7yhcyvpt5wmny4sh6sbvm","timestamp":1712925254,"version":1}