Skip to content

Commit

Permalink
Beginnings of a DecentSablier module
Browse files Browse the repository at this point in the history
  • Loading branch information
adamgall committed Sep 5, 2024
1 parent c0468db commit 1617792
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
42 changes: 42 additions & 0 deletions contracts/DecentSablier_0_1_0.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.8.19;

import {Enum} from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
import {IAvatar} from "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol";
import {ISablier} from "./interfaces/sablier/ISablier.sol";

contract DecentSablier_0_1_0 {
string public constant NAME = "DecentSablier_0_1_0";

struct SablierStreamInfo {
uint256 streamId;
}

function processSablierStreams(
address sablierContract,
SablierStreamInfo[] calldata streams
) public {
ISablier sablier = ISablier(sablierContract);

for (uint256 i = 0; i < streams.length; i++) {
uint256 streamId = streams[i].streamId;

// Get the current balance available for withdrawal
uint256 availableBalance = sablier.balanceOf(streamId, msg.sender);

if (availableBalance > 0) {
// Proxy the withdrawal call through the Safe
IAvatar(msg.sender).execTransactionFromModule(
sablierContract,
0,
abi.encodeWithSelector(
ISablier.withdrawFromStream.selector,
streamId,
availableBalance
),
Enum.Operation.Call
);
}
}
}
}
28 changes: 28 additions & 0 deletions contracts/interfaces/sablier/ISablier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.8.19;

interface ISablier {
function getStream(
uint256 streamId
)
external
view
returns (
address sender,
address recipient,
uint256 deposit,
address tokenAddress,
uint256 startTime,
uint256 stopTime,
uint256 remainingBalance,
uint256 ratePerSecond
);
function balanceOf(
uint256 streamId,
address who
) external view returns (uint256 balance);
function withdrawFromStream(
uint256 streamId,
uint256 amount
) external returns (bool);
}
34 changes: 34 additions & 0 deletions contracts/mock/MockSablier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.8.19;

contract MockSablier {
mapping(uint256 => uint256) private streamBalances;
mapping(uint256 => uint256) private withdrawnAmounts;

function setStreamBalance(uint256 streamId, uint256 balance) external {
streamBalances[streamId] = balance;
}

function balanceOf(
uint256 streamId,
address
) external view returns (uint256) {
return streamBalances[streamId];
}

function withdrawFromStream(
uint256 streamId,
uint256 amount
) external returns (bool) {
require(streamBalances[streamId] >= amount, "Insufficient balance");
streamBalances[streamId] -= amount;
withdrawnAmounts[streamId] += amount;
return true;
}

function getWithdrawnAmount(
uint256 streamId
) external view returns (uint256) {
return withdrawnAmounts[streamId];
}
}
143 changes: 143 additions & 0 deletions test/DecentSablier_0_1_0.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
GnosisSafeL2,
GnosisSafeL2__factory,
DecentSablier_0_1_0__factory,
DecentSablier_0_1_0,
} from "../typechain-types";

import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "ethers";
import hre from "hardhat";

import {
getGnosisSafeL2Singleton,
getGnosisSafeProxyFactory,
} from "./GlobalSafeDeployments.test";
import {
buildSafeTransaction,
buildSignatureBytes,
predictGnosisSafeAddress,
safeSignTypedData,
} from "./helpers";

import { MockSablier__factory } from "../typechain-types";

async function executeSafeTransaction({
safe,
to,
value,
data,
operation,
signers,
}: {
safe: GnosisSafeL2;
to: string;
value?: bigint;
data?: string;
operation?: number;
signers: SignerWithAddress[];
}) {
const safeTransactionData = {
to,
value: value || 0n,
data: data || "0x",
operation: operation || 0,
// Add the missing 'nonce' property
nonce: await safe.nonce(),
};
const safeTransaction = await buildSafeTransaction(safeTransactionData);
const senderSignature = await safeSignTypedData(
signers[0],
safe,
safeTransaction
);
const signatureBytes = buildSignatureBytes([senderSignature]);
// Change 'executeTransaction' to 'execTransaction'
return safe.execTransaction(safeTransaction, signatureBytes);
}

describe("DecentSablier", () => {
let dao: SignerWithAddress;
let gnosisSafe: GnosisSafeL2;
let decentSablier: DecentSablier_0_1_0;
let decentSablierAddress: string;
let gnosisSafeAddress: string;

let mockSablier: MockSablier;

beforeEach(async () => {
// ... (setup code similar to DecentHats.test.ts)
// Deploy MockSablier
const MockSablier = await ethers.getContractFactory("MockSablier");
mockSablier = await MockSablier.deploy();
await mockSablier.deployed();
});

describe("DecentSablier as a Module", () => {
let enableModuleTx: ethers.ContractTransactionResponse;

beforeEach(async () => {
// ... (enable module code similar to DecentHats.test.ts)
});

it("Emits an ExecutionSuccess event", async () => {
await expect(enableModuleTx).to.emit(gnosisSafe, "ExecutionSuccess");
});

it("Emits an EnabledModule event", async () => {
await expect(enableModuleTx)
.to.emit(gnosisSafe, "EnabledModule")
.withArgs(decentSablierAddress);
});

describe("Processing Sablier Streams", () => {
let processSablierStreamsTx: ethers.ContractTransactionResponse;

beforeEach(async () => {
// Set up mock stream balances
await mockSablier.setStreamBalance(1, ethers.utils.parseEther("100"));
await mockSablier.setStreamBalance(2, ethers.utils.parseEther("200"));
await mockSablier.setStreamBalance(3, ethers.utils.parseEther("300"));

processSablierStreamsTx = await executeSafeTransaction({
safe: gnosisSafe,
to: decentSablierAddress,
data: DecentSablier_0_1_0__factory.createInterface().encodeFunctionData(
"processSablierStreams",
[
mockSablier.address,
[{ streamId: 1 }, { streamId: 2 }, { streamId: 3 }],
]
),
signers: [dao],
});
});

it("Emits an ExecutionSuccess event", async () => {
await expect(processSablierStreamsTx).to.emit(
gnosisSafe,
"ExecutionSuccess"
);
});

it("Emits an ExecutionFromModuleSuccess event", async () => {
await expect(processSablierStreamsTx)
.to.emit(gnosisSafe, "ExecutionFromModuleSuccess")
.withArgs(decentSablierAddress);
});

it("Withdraws from streams correctly", async () => {
expect(await mockSablier.getWithdrawnAmount(1)).to.equal(
ethers.utils.parseEther("100")
);
expect(await mockSablier.getWithdrawnAmount(2)).to.equal(
ethers.utils.parseEther("200")
);
expect(await mockSablier.getWithdrawnAmount(3)).to.equal(
ethers.utils.parseEther("300")
);
});
});
});
});

0 comments on commit 1617792

Please sign in to comment.