38.LostAssets
2023-06-23 20:22:31 # 00.security

LostAssets

题目

要求:调用isComplete()返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
pragma solidity 0.8.10;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Permit, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";

contract MockWETH is ERC20("Wrapped ETH", "WETH") {
event Deposit(address indexed dst, uint256 wad);
event Withdrawal(address indexed src, uint256 wad);

/// @dev Original WETH9 implements `fallback` function instead of `receive` function due to a earlier solidity version
fallback() external payable {
deposit();
}

function deposit() public payable {
_mint(msg.sender, msg.value);

emit Deposit(msg.sender, msg.value);
}

function withdraw(uint256 wad) public {
require(balanceOf(msg.sender) >= wad, "weth: insufficient balance");

_burn(msg.sender, wad);
(bool success, ) = msg.sender.call{value: wad}("");
require(success, "weth: failed");

emit Withdrawal(msg.sender, wad);
}
}

/// @notice Token sWETH
contract MocksWETH is ERC20Permit {
using SafeERC20 for IERC20;

address underlying;

constructor(address _underlying)
ERC20("WrappedERC20", "WERC20")
ERC20Permit("WrappedERC20")
{
underlying = _underlying;
}

function deposit() external returns (uint256) {
uint256 _amount = IERC20(underlying).balanceOf(msg.sender);
IERC20(underlying).safeTransferFrom(msg.sender, address(this), _amount);
return _deposit(_amount, msg.sender);
}

function deposit(uint256 amount) external returns (uint256) {
IERC20(underlying).safeTransferFrom(msg.sender, address(this), amount);
return _deposit(amount, msg.sender);
}

function depositWithPermit(
address target,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
address to
) external returns (uint256) {
// permit is an alternative to the standard approve call:
// it allows an off-chain secure signature to be used to register an allowance.
// The permitter is approving the beneficiary to spend their money, by signing the permit request
IERC20Permit(underlying).permit(
target,
address(this),
value,
deadline,
v,
r,
s
);
IERC20(underlying).safeTransferFrom(target, address(this), value);
return _deposit(value, to);
}

function _deposit(uint256 value, address to) internal returns (uint256) {
_mint(to, value);
return value;
}

/// @notice withdraw all
function withdraw() external returns (uint256) {
return _withdraw(msg.sender, balanceOf(msg.sender), msg.sender);
}

/// @notice withdraw specified `amount`
function withdraw(uint256 amount) external returns (uint256) {
return _withdraw(msg.sender, amount, msg.sender);
}

function _withdraw(
address from,
uint256 amount,
address to
) internal returns (uint256) {
_burn(from, amount);
IERC20(underlying).safeTransfer(to, amount);
return amount;
}
}

contract LostAssets {
MockWETH public WETH;
MocksWETH public sWETH;
constructor() payable {
require(msg.value >= 1 ether, "At least 1 ether");//
WETH = new MockWETH();
sWETH = new MocksWETH(address(WETH));
WETH.deposit{value: msg.value}();//假设我们就发送1ETH,LostAssets合约 在 ETH合约 拥有1ETH
// Guaranteed interchangeability of WETH and sWETH
WETH.approve(address(sWETH), type(uint256).max);//approve金额给sWETH
// sWETH.approve(address(WETH), type(uint256).max); // WETH cannot use approval
// Deposit half of weth balance
sWETH.deposit(msg.value / 2);// sWETH deposit 了 0.5ETH,还剩 0.5ETH 授权可用

//因此我们需要获得sWETH的授权,获得0.5ETH授权。
//授权情况:LostAssets => sWETH => 我的钱包地址。
//因为sWETH有一个方法depositWithPermit,我们是否可以用这个方法在链下进行授权,
//然后实现:我的钱包代理 LostAssets剩余的0.5ETH,然后划走0.5ETH给其他用户?
//因此我们先要进行签名授权,但是如何将 LostAssets合约 签名给我的地址呢?从而拿到v,r,s
}

function isComplete() public view returns (bool) {
require(WETH.balanceOf(address(this)) == 0);
return true;
}
}

分析

题目包括三个合约:MockWETH.sol,MocksWETH.sol,LostAssets.sol。

项目的意思是:

  • MockWETH和MocksWETH的ERC20代币之间具有流动性
  • 用户可以在MockWETH进行存款与提款,也可以将用户在MockWETH的权益转移到MocksWETH。

  • 但是在MocksWETH无法提款,它的提款操作是将权益转回到MockWETH。用户在MockWETH才能真正的提款发送以太

我们的任务:成功调用ClostAssets.sol的isComplete()并且返回true

1.分析题目的目标

isComplete()要求WETH.balanceOf(address(this)) == 0,也就是将LostAssets合约在MockWETH的余额设置为0

我们设置msg.value=1ETH,在LostAssets合约的构造器中做了以下操作

  1. require(msg.value >= 1 ether, "At least 1 ether");:msg.value至少为1ETH
  2. WETH = new MockWETH();sWETH = new MocksWETH(address(WETH));:新建了MockWETH和MocksWETH
  3. WETH.deposit{value: msg.value}();:msg.sender是LostAssets,所以LostAssets在MockWETH中有1ETH
  4. WETH.approve(address(sWETH), type(uint256).max);LostAssets授权给sWETH最大值金额,这就意味着,sWETH可以对LostAssets在MoskWETH的金额做任何操作,那么后面只要我们调用TransferFrom可以操作余额
  5. sWETH.deposit(msg.value / 2);:msg.sender是LostAssets,LostAssets将其在WETH的1ETH划走0.5ETH到sWETH,这就体现了流动性

我们的目的是将LostAssets在WETH剩余的0.5ETH也划走,也就是设置为0

2.分析合约

MockWETH合约

这个合约继承了@openzeppelin的ERC20,写了一个存款函数deposit()和一个提款函数withdraw(uint256 wad),并且发现提款函数并没有重入攻击的可能性,并且版本是0.8.10,也没有溢出漏洞的可能性

MocksWETH合约

此合约将MockWETH传入,并且通过using SafeERC20 for IERC20;,在每一个存款和提款方法使用underlying地址对MockWETH合约进行操作:验证是否在MockWETH合约真的有这么多钱,才给存款;将在MocksWETH合约的余额重新划入MockWETH,然后进一步在MockWETH合约进行提款获取以太

3.分析解题

3.1错误想法

因为LostAssets approve给MocksWETH了很大的金额,因此MocksWETH可以划走剩余的0.5ETH,但是合约不会主动去调用,我们无法直接通过让MocksWETH主动来转走0.5ETH

但是MocksWETH当中有一个depositWithPermit()函数,意思是可以进行链下签名授权,然后再链上再进行操作,这个是eip-2612eip-712相关的内容。eip-2612和eip-712的permit是允许一个EOA账户进行链下签名然后链上操作,但题意是想将LostAssets的0.5ETH进行代理,无论是LostAssets还是被授权给的MocksWETH都是一个合约,不是EOA账户,因此无法进行permit签名!【合约虽然也有私钥,理论上可以进行签名,但是eip协议后来又规定合约签名出来的交易无效,并且想要获取一个合约的私钥是不可能的,只能暴力破解】

又因为题目给的合约并没有实现eip-1271,因此无法使用合约链下签名

题目的考点应该是permit(),但是我认为无法通过permit()进行链下操作而解题的。题目我又找不到相关的方法可以进行操作,因为继承了@openzeppelin的ERC20、ERC20Permit,基本上是没什么问题的,只有这么一个permit()可以进行操作,然而这个可能性也没了

我觉得可能是出错题了,我觉得题目可以有如下两种改法:

  1. 题目从” 将合约拥有的钱设置为0 “改成“ 将EOA拥有的钱设置为0 “,那么才有可能用到permit函数”
  2. 题目不是实现ERC20Permit,而是实现eip-1271,那么就可以利用合约进行链下签名了,即LostAssets可以进行签名授权0.5ETH

这是我在stackoverflow的提问

3.2正确想法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function depositWithPermit(
address target,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
address to
) external returns (uint256) {
// permit is an alternative to the standard approve call:
// it allows an off-chain secure signature to be used to register an allowance.
// The permitter is approving the beneficiary to spend their money, by signing the permit request
IERC20Permit(underlying).permit(
target,
address(this),
value,
deadline,
v,
r,
s
);
IERC20(underlying).safeTransferFrom(target, address(this), value);
return _deposit(value, to);
}

解题关键还是depositWithPermit()。我们发现,IERC20Permit(underlying).permit方法,underlying是contract MockWETH is ERC20("Wrapped ETH", "WETH"),它并没有permit方法,因此会进入fallback,执行deposit函数,但是对我们转移代币没什么作用,只会_mint记录一下财产情况

IERC20(underlying).safeTransferFrom(target, address(this), value)是关键:因为using SafeERC20 for IERC20;且import了相关合约,这个方法会进入方法体执行执行。先前WETH.approve(address(sWETH), type(uint256).max);LostAssets已经授权最大值给sWETH了,因此这里sWETH可以进行safeTransferFrom来代理LostAssets的财产。于是我们可以将剩余的一半余额进行转移到其他任意地址,这样余额就为0满足题目要求了

注意:我调用MocksWETH,depositWithPermit方法中又IERC20(underlying).safeTransferFrom(target, address(this), value)进行调用,在safeTransferFrom中,msg.sender不是我,而是MocksWETH

做题

1.部署题目

image-20230127233543493

2.调用depositWithPermit

image-20230127233743733

3.成功修改

image-20230127233814246

总结

本题涉及ERC20,ERC20Permit,相关的内容是eip-1271eip-712eip-2612,ecrecover还原signature,ERC20之间的互操作性。实际上是逻辑上的错误,感觉题目并没有太大的意义,先是授权了最大值,然后depositWithPermit当中又转账

Prev
2023-06-23 20:22:31 # 00.security
Next