通过阅读本文你可以学到:
如何实现一个通用的、可复用的薅羊毛合约(仅供参考,本人是 Solidity 菜鸡, 相关代码可以在
<https://github.com/neal-zhu/batcher >
找到)
对于一些空投 token,free mint nft 项目,项目方有时候可能会无意(甚至是有意)的给科学家留下发挥的空间。这方面比较出名的有早前的小学生项目 RND,近有最近罕见的热点项目 Xen。这些项目的主要特点是合约代码中未进行 tx.origin == msg.sender 的判断。
有文章
<https://mirror.xyz/hackbot.eth/plbO7co90A6JhKSRSMsstrcP-X8CpfnaRs7k3HI3loY >
中有介绍如何通过运行时创建大量子合约来批量领取大量 RND 代币。主要代码如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
// import "hardhat/console.sol";
interface airdrop {
function transfer(address recipient, uint256 amount) external;
function balanceOf(address account) external view returns (uint256);
function claim() external;
}
contract multiCall{
// 此处填写 RND 代币的合约地址
address constant contra = address(0xcb33F7FB101E377a4b0e19fD647F391fAD14d0B5);
function call(uint256 times) public {
for(uint i=0;i<times;++i){
new claimer(contra);
}
}
}
contract claimer{
constructor(address contra){
airdrop(contra).claim();
uint256 balance = airdrop(contra).balanceOf(address(this));
airdrop(contra).transfer(address(tx.origin), balance);
selfdestruct(payable(address(msg.sender)));
}
}
上面的代码实现清晰明了,但是也有一个明显的问题,即不具备可复用性。因为项目方不同,而且在空投这件事情上,也并不存在所谓的 ERCxxx 标准,所以可以撸 RND 项目的合约代码,往往在其他项目上就失效了。这也是为什么这种合约往往在撸毛结束以后,都会销毁批量创建的子合约。
这样导致了一些问题:
面对不同项目时候,需要做更多重复的工作(部署合约,对合约进行小修改)
多个项目,反复的批量创建、销毁合约, gas 消耗会更高
大家都在撸的时候,进行的一些列包括部署、调用等操作,成本会更高
比如最近大火的 Xen,除了合约地址不同,其交互流程也更为复杂:
调用 claimRank(uint256)
挖矿时间足够以后,调用 claimMintReward() or claimMintRewardAndShare(address, uint256)
大家只好再起炉灶,赶紧写一份大体一致的代码,然后开始部署调用等操作。比如下面这份用来撸 Xen 的代码:
/**
*Submitted for verification at Etherscan.io on 2022-10-10
*/
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface IXEN1{
function claimRank(uint256 term) external;
function claimMintReward() external;
function approve(address spender, uint256 amount) external returns (bool);
}
interface IXEN2{
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract GET{
IXEN1 private constant xen = IXEN1(0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8);
constructor() {
xen.approve(msg.sender,~uint256(0));
}
function claimRank(uint256 term) public {
xen.claimRank(term);
}
function claimMintReward() public {
xen.claimMintReward();
selfdestruct(payable(tx.origin));
}
}
/// @author 捕鲸船社区 加入社区添加微信:Whaler_man 关注推特 @Whaler_DAO
contract GETXEN {
mapping (address=>mapping (uint256=>address[])) public userContracts;
IXEN2 private constant xen = IXEN2(0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8);
address private constant whaler = 0x918Cb3c935d82eE20F4986158dFA755048F41d47;
function claimRank(uint256 times, uint256 term) external {
address user = tx.origin;
for(uint256 i; i<times; ++i){
GET get = new GET();
get.claimRank(term);
userContracts[user][term].push(address(get));
}
}
function claimMintReward(uint256 times, uint256 term) external {
address user = tx.origin;
for(uint256 i; i<times; ++i){
uint256 count = userContracts[user][term].length;
address get = userContracts[user][term][count - 1];
GET(get).claimMintReward();
address owner = tx.origin;
uint256 balance = xen.balanceOf(get);
xen.transferFrom(get, whaler, balance * 10 / 100);
xen.transferFrom(get, owner, balance * 90 / 100);
userContracts[user][term].pop();
}
}
}
不难发现,两份合约代码主体框架是极为相似的,完全可以用一种更加通用的合约来尝试替代。实现思路也很简单:尽量让所有 hardcode 的部分动态化,作为参数传入。同时,因为这些合约已经可以复用,我们不再每次都动态批量创建合约,而是可以反复复用。实现的代码也很简单:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
import "hardhat/console.sol";
// ERC20 interface
interface ERC20 {
function transfer(address recipient, uint256 amount) external;
function balanceOf(address account) external view returns (uint256);
}
// Proxy contract to execute multiple transactions
contract Proxy {
address public owner;
constructor(address _owner) {
owner = _owner;
}
// only owner modifier
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}
// withdraw all tokens
function withdraw(address token) public onlyOwner {
uint256 balance = ERC20(token).balanceOf(address(this));
ERC20(token).transfer(tx.origin, balance);
}
// withdraw all ETH
function withdrawETH() public onlyOwner {
payable(tx.origin).transfer(address(this).balance);
}
// execute encodeed transaction
function execute(address target, bytes memory data)
public
payable
onlyOwner
{
(bool success, ) = target.call(data);
require(success, "Transaction failed.");
}
// Destroys this contract instance
function destroy(address payable recipient) public onlyOwner {
selfdestruct(recipient);
}
}
contract Batcher {
address public owner;
Proxy[] public proxies;
constructor(uint256 _n) {
owner = msg.sender;
// create proxy contracts, we will not destroy them
for (uint256 i = 0; i < _n; i++) {
// create with salt
Proxy proxy = new Proxy{salt: bytes32(uint256(i))}(address(this));
// append to proxies
proxies.push(proxy);
}
}
function getBytecode() public view returns (bytes memory) {
bytes memory bytecode = type(Proxy).creationCode;
return abi.encodePacked(bytecode, abi.encode(msg.sender));
}
function getAddress(uint256 _salt) public view returns (address) {
// Get a hash concatenating args passed to encodePacked
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 0
address(this), // address of factory contract
_salt, // a random salt
keccak256(getBytecode()) // the wallet contract bytecode
)
);
// Cast last 20 bytes of hash to address
return address(uint160(uint256(hash)));
}
fallback() external payable {
require(owner == msg.sender, "Only owner can call this function.");
// delegatecall to proxy contracts
for (uint256 i = 0; i < proxies.length; i++) {
address proxy = address(proxies[i]);
(bool success, ) = proxy.call(msg.data);
require(success, "Transaction failed.");
}
}
receive() external payable {}
}
在我们的合约中主要有两个角色:
Proxy,即批量创建的合约,其主要作用就是动态的执行任何传入的 tx 数据,这样我们可以让 Proxy 以任何我们想要的姿势和项目方代码进行交互。
Batcher,门面合约,负责将交互 tx 转发到所有创建的 Proxy。任何对此合约的调用,最终都会转换为在 N 个 Proxy 合约上的操作。
以交互 Xen 为例,我们需要在 hardhat 中添加类似代码来实现交互:
require("@nomicfoundation/hardhat-toolbox");
const xenAddress = "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8";
const xenABI = [] // skip
const batcherABI = [
"function execute(address, bytes) payable",
"function withdraw(address)",
"function withdrawETH(address)",
"function destroy()",
]
async function increaseTime(value) {
if (!ethers.BigNumber.isBigNumber(value)) {
value = ethers.BigNumber.from(value);
}
await ethers.provider.send('evm_increaseTime', [value.toNumber()]);
await ethers.provider.send('evm_mine');
}
task("xen_claim_rank", "claimRank for xen").setAction(async (taskArgs, hre) => {
const signer = (await hre.ethers.getSigners())[0];
const xen = new hre.ethers.Contract(xenAddress, xenABI, signer);
const batcher = new hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
let tx = await batcher.execute(xenAddress, xen.interface.encodeFunctionData("claimRank", [1]));
await tx.wait();
console.log("XEN balance: ", (await xen.balanceOf(signer.address)).toString());
}).addParam("batcher", "address of batcher contract")
task("xen_claim_reward", "claim reward for xen").setAction(async (taskArgs, hre) => {
const signer = (await hre.ethers.getSigners())[0];
const xen = new hre.ethers.Contract(xenAddress, xenABI, signer);
// increase and mine time so we can claimReward
// FOR TEST ONLY
await increaseTime(24*60*60);
const batcher = new hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
tx = await batcher.execute(xen.address, xen.interface.encodeFunctionData("claimMintRewardAndShare", [signer.address, 100]));
await tx.wait();
console.log("XEN balance: ", (await xen.balanceOf(signer.address)).toString());
}).addParam("batcher", "address of batcher contract")
我们只需要依次执行:
npx hardhat --network localhost run scripts/deploy.js # 如果已经部署过 batcher 合约,则不需要重复部署
npx hardhat --network localhost xen_claim_rank --batcher {batcherAddress}
npx hardhat --network localhost xen_claim_reward --batcher {batcherAddress}
观察 hardhat 中的脚本代码,不难看出,我们这次的重构本质是: 将链上代码搬移到链下。本来写在 Solidity 合约中的业务逻辑,出现在了 javascript 脚本中。
最后来总结下改造之后对比之前方案的优劣:
优点:
可以在 gas 低的时候创建好批量合约,等热点项目出来的时候降低 gas 成本
减少了部分重复性操作(比如大体框架一致的合约代码开发,新合约部署等)
javascript 代码的调试、迭代比 Solidity 更加便利
缺点: