Solidity 智能合约透明代理升级实战
1.代理升级模式
代理升级模式包括不同的实现方式,主要有透明代理、UUPS 代理和 Beacon 代理。每种模式都有其特点和适用场景,本文我们来给大家讲解透明代理升级的特点与实战
2.透明代理简介与特点
透明代理模式利用两个合约——代理合约和逻辑合约。代理合约负责管理对逻辑合约的调用,并可以进行升级。透明代理模式中,代理合约和逻辑合约之间的交互通过 delegatecall 实现,且合约的逻辑和存储被分离开来;透明代理升级具有以下特点:
- 代理合约可以被升级,但原始合约地址不变。
- 需要管理员权限来执行合约的升级。
- 透明代理模式简单易用,但升级的管理可能会带来一些安全隐患(如合约被误升级等)。
3.透明代理升级实战代码-社区任务赏金管理合约
3.1合约逻辑
- 项目方可以往合约里面充值 ETH 或者其他 ERC20 Token
- 社区成员完成一个任务之后,链下服务会触发向任务管理合约提交该任务的赏金
- 社区成员可以从合约里面 cliam 自己的赏金
- 项目可以从合约里面提取 ETH 或者是 ERC20 Token
3.2.代码
3.2.1.interfaces 代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface ITreasureManager {
function depositETH() external payable returns (bool);
function depositERC20(IERC20 tokenAddress, uint256 amount) external returns (bool);
function grantRewards(address tokenAddress, address granter, uint256 amount) external;
function claimAllTokens() external;
function claimToken(address tokenAddress) external;
function withdrawETH(address payable withdrawAddress, uint256 amount) external payable returns (bool);
function withdrawERC20(IERC20 tokenAddress, address withdrawAddress, uint256 amount) external returns (bool);
function setTokenWhiteList(address tokenAddress) external;
function setWithdrawManager(address _withdrawManager) external;
function queryReward(address _tokenAddress) external view returns (uint256);
function getTokenWhiteList() external view returns (address[] memory);
}
3.2.2.合约代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ITreasureManager.sol";
contract TreasureManager is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable, ITreasureManager {
using SafeERC20 for IERC20;
address public constant ethAddress = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
address public treasureManager;
address public withdrawManager;
address[] public tokenWhiteList;
mapping(address => uint256) public tokenBalances;
mapping(address => mapping(address => uint256)) public userRewardAmounts;
error IsZeroAddress();
event DepositToken(
address indexed tokenAddress,
address indexed sender,
uint256 amount
);
event WithdrawToken(
address indexed tokenAddress,
address sender,
address withdrawAddress,
uint256 amount
);
event GrantRewardTokenAmount(
address indexed tokenAddress,
address granter,
uint256 amount
);
event WithdrawManagerUpdate(
address indexed withdrawManager
);
modifier onlyTreasureManager() {
require(msg.sender == address(treasureManager), "TreasureManager.onlyTreasureManager");
_;
}
modifier onlyWithdrawManager() {
require(msg.sender == address(withdrawManager), "TreasureManager.onlyWithdrawer");
_;
}
function initialize(address _initialOwner, address _treasureManager, address _withdrawManager) public initializer {
treasureManager = _treasureManager;
withdrawManager = _withdrawManager;
_transferOwnership(_initialOwner);
}
receive() external payable {
depositETH();
}
function depositETH() public payable nonReentrant returns (bool) {
tokenBalances[ethAddress] += msg.value;
emit DepositToken(
ethAddress,
msg.sender,
msg.value
);
return true;
}
function depositERC20(IERC20 tokenAddress, uint256 amount) external returns (bool) {
tokenAddress.safeTransferFrom(msg.sender, address(this), amount);
tokenBalances[address(tokenAddress)] += amount;
emit DepositToken(
address(tokenAddress),
msg.sender,
amount
);
return true;
}
function grantRewards(address tokenAddress, address granter, uint256 amount) external onlyTreasureManager {
require(address(tokenAddress) != address(0) && granter != address(0), "Invalid address");
userRewardAmounts[granter][address(tokenAddress)] += amount;
emit GrantRewardTokenAmount(address(tokenAddress), granter, amount);
}
function claimAllTokens() external {
for (uint256 i = 0; i < tokenWhiteList.length; i++) {
address tokenAddress = tokenWhiteList[i];
uint256 rewardAmount = userRewardAmounts[msg.sender][tokenAddress];
if (rewardAmount > 0) {
if (tokenAddress == ethAddress) {
(bool success, ) = msg.sender.call{value: rewardAmount}("");
require(success, "ETH transfer failed");
} else {
IERC20(tokenAddress).safeTransfer(msg.sender, rewardAmount);
}
userRewardAmounts[msg.sender][tokenAddress] = 0;
tokenBalances[tokenAddress] -= rewardAmount;
}
}
}
function claimToken(address tokenAddress) external {
require(tokenAddress != address(0), "Invalid token address");
uint256 rewardAmount = userRewardAmounts[msg.sender][tokenAddress];
require(rewardAmount > 0, "No reward available");
if (tokenAddress == ethAddress) {
(bool success, ) = msg.sender.call{value: rewardAmount}("");
require(success, "ETH transfer failed");
} else {
IERC20(tokenAddress).safeTransfer(msg.sender, rewardAmount);
}
userRewardAmounts[msg.sender][tokenAddress] = 0;
tokenBalances[tokenAddress] -= rewardAmount;
}
function withdrawETH(address payable withdrawAddress, uint256 amount) external payable onlyWithdrawManager returns (bool) {
require(address(this).balance >= amount, "Insufficient ETH balance in contract");
(bool success, ) = withdrawAddress.call{value: amount}("");
if (!success) {
return false;
}
tokenBalances[ethAddress] -= amount;
emit WithdrawToken(
ethAddress,
msg.sender,
withdrawAddress,
amount
);
return true;
}
function withdrawERC20(IERC20 tokenAddress, address withdrawAddress, uint256 amount) external onlyWithdrawManager returns (bool) {
require(tokenBalances[address(tokenAddress)] >= amount, "Insufficient token balance in contract");
tokenAddress.safeTransfer(withdrawAddress, amount);
tokenBalances[address(tokenAddress)] -= amount;
emit WithdrawToken(
address(tokenAddress),
msg.sender,
withdrawAddress,
amount
);
return true;
}
function setTokenWhiteList(address tokenAddress) external onlyTreasureManager {
if(tokenAddress == address(0)) {
revert IsZeroAddress();
}
tokenWhiteList.push(tokenAddress);
}
function getTokenWhiteList() external view returns (address[] memory) {
return tokenWhiteList;
}
function setWithdrawManager(address _withdrawManager) external onlyOwner {
withdrawManager = _withdrawManager;
emit WithdrawManagerUpdate(
withdrawManager
);
}
function queryReward(address _tokenAddress) public view returns (uint256) {
return userRewardAmounts[msg.sender][_tokenAddress];
}
}
3.2.3.部署脚本
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Script, console } from "forge-std/Script.sol";
import "../src/TreasureManager.sol";
contract TreasureManagerScript is Script {
ProxyAdmin public dapplinkProxyAdmin;
TreasureManager public treasureManager;
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);
dapplinkProxyAdmin = new ProxyAdmin(deployerAddress);
console.log("PdapplinkProxyAdmin:", address(dapplinkProxyAdmin));
treasureManager = new TreasureManager();
TransparentUpgradeableProxy proxyTreasureManager = new TransparentUpgradeableProxy(
address(treasureManager),
address(dapplinkProxyAdmin),
abi.encodeWithSelector(TreasureManager.initialize.selector, deployerAddress, deployerAddress, deployerAddress)
);
console.log("TransparentUpgradeableProxy deployed at:", address(proxyTreasureManager));
vm.stopBroadcast();
}
}
部署脚本执行命令
forge script script/TreasureManager.s.sol:TreasureManagerScript --rpc-url $RPC_URL --private-key $PRIVATE_
KEY --broadcast -vvvv
请将 RPC_URL 和 PRIVATE_KEY 环境变量配置成您自己的环境变量。
3.2.4.升级脚本
如果需要升级,改变逻辑合约之后,写升级脚本如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Script, console } from "forge-std/Script.sol";
import "../src/TreasureManager.sol";
contract TreasureManagerV2 is Script {
function run() public {
address proxyAddmin = 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853;
address proxyTreasureManager = 0x8A791620dd6260079BF849Dc5567aDC3F2FdC318;
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);
TreasureManager treasureManagerV2 = new TreasureManager();
console.log("treasureManagerV2:", address(treasureManagerV2));
ProxyAdmin(proxyAddmin).upgradeAndCall(ITransparentUpgradeableProxy(proxyTreasureManager), address(treasureManagerV2), bytes(""));
vm.stopBroadcast();
}
}
脚本部署命令
forge script script/TreasureManagerV2.s.sol:TreasureManagerV2 --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast -vvvv